openlang

expr를 감싸는 중괄호가 바이트코드 컴파일러가 보는 것을 정한다

중괄호 없는 expr는 할 수만 있다면 Tcl에서 금지하고 싶은 습관이에요! 스타일 지적처럼 보이지만 전혀 아니에요.

set qty {[puts "side effect"; list 3]}
puts [expr {$qty == 3}]
puts [expr $qty == 3]

중괄호 버전은 0을 찍어요. 바이트코드 컴파일러가 $qty == 3을 식으로 보고, 변수를 한 번 읽고, 이상한 문자열을 3과 비교해요. 중괄호 없는 버전은 side effect를 찍고 나서 1을 찍어요. Tcl이 명령의 단어를 만들면서 $qty를 치환하니까 expr는 [puts "side effect"; list 3] == 3이라는 텍스트를 받고, 거기서 자기 차례의 치환을 한 번 더 해요. 그때 대괄호 안의 명령이 실행돼요. 치환이 두 번이고, 두 번째 치환은 변수에 들어 있던 걸 뭐든 실행해요.

여기까지가 재밌는 부분이고, 조용한 부분은 속도예요. 중괄호 없는 형태는 미리 컴파일할 수 없어서 실행될 때마다 파싱돼요. 여기선 9.1에서 확인했지만, 이건 아주 오래전부터 그랬어요.

이어지는 대화 13

서로의 근거를 따라
  1. 고유 링크

    중괄호 버전도 한 번 더 읽어 볼 만해요. ==도 뭔가 하고 있거든요. 양쪽이 숫자처럼 보이면 숫자로 비교하고, 아니면 문자열 비교로 넘어가요. 이상한 문자열이 오류 대신 그냥 0을 주는 이유가 그거예요. 그 넘어가는 동작은 따로 문제를 일으켜요. expr {"1.0" == "1"}은 1이고 expr {"1.0" eq "1"}은 0이에요. 텍스트를 비교하려는 거라면 eq가 그 뜻을 그대로 말하고, 숫자로 읽으려는 시도도 안 해요.

  2. 고유 링크

    if나 while에서도 같은 일이 생기나요? 그 조건들도 식이잖아요. 오래된 스크립트에서 if $done을 생각보다 자주 봐요.

  3. 고유 링크

    한 줄짜리 프로시저 두 개에 tcl::unsupported::disassemble을 돌려서 세어 봤어요. expr {$a + $b}는 명령어 네 개로 컴파일돼요. loadScalar 두 개, add 하나, done 하나예요. expr $a + $b는 여덟 개예요. a를 읽고, 공백과 +를 리터럴로 넣고, b를 읽고, 다섯 조각을 strcat으로 이어서 그 결과를 exprStk에 넘겨요. 이 명령어는 호출될 때마다 실행 중에 식을 파싱하고 돌려요. 숫자에 대한 답은 같고, 두 번째는 호출마다 문자열 조립과 파싱을 한 번씩 해요.

  4. 고유 링크

    네, 둘 다요! if $done은 expr처럼 치환이 두 번 일어나요. while은 더 나빠요. 조건이 루프 시작 전에 딱 한 번 치환되거든요.

    set i 0
    set guard 0
    while "$i < 3" {
        incr i
        if {[incr guard] > 10} {
            puts "still looping, i is $i"
            break
        }
    }

    따옴표 때문에 조건은 while이 실행되기도 전에 0 < 3이 되고, 그래서 while은 상수를 계속 다시 평가하고 루프는 끝나지 않아요(guard는 데모가 멈추라고 넣은 거예요). {$i < 3}로 쓰면 매번 i를 다시 읽어요. for와 그 조건도 마찬가지예요.

  5. 고유 링크

    중괄호와는 별개로, 9.1에서는 expr {010 + 0}이 10이고 8.6에서는 앞의 0 때문에 8진수 8이었어요. 두 버전 모두에서 8을 뜻하는 건 0o10이라서, 0으로 채운 ID를 찾아보게 됐어요.

  6. 고유 링크

    같은 이중 치환은 문자열을 이어 붙여 스크립트를 만드는 C 호스트에도 미칩니다. sprintf로 조립해 Tcl_Eval에 넘긴 명령은 스크립트 텍스트로 파싱되므로, 대괄호가 든 값이 실행됩니다. Tcl_EvalObjv는 명령을 단어 배열로 받고 그 단어들을 다시 파싱하거나 치환하지 않으므로, 값은 어떤 문자를 담고 있든 단어 하나로 남습니다. 이것이 C 쪽에서 중괄호에 해당하는 방법입니다. 다만 그 단어 중 하나가 그 자체로 스크립트라면, 예컨대 eval에 넘기는 본문이나 중괄호 없는 expr라면 도움이 되지 않으므로, 단어를 받는 Tcl 코드 안에서도 같은 규칙이 지켜져야 합니다.

  7. 고유 링크

    저한테 실제로 일어날 법한 버전은 이거예요. 작은 Tk 폼에 수량 입력칸이 있고, 합계를 expr $qty * $price로 계산하는데, 누가 뭐가 되나 보려고 수량 칸에 [clock seconds]를 붙여 넣어요(사람들은 입력칸에 진짜 이상한 걸 붙여 넣어요). 합계가 엄청난 숫자가 되고 아무도 그게 어디서 왔는지 몰라요. 중괄호를 쓰면 치환은 해결돼요. 그래도 값이 expr 근처에 가기 전에 입력칸에서 숫자가 아닌 걸 거절해야 할까요, 아니면 중괄호만으로 충분할까요?

  8. 고유 링크

    흔히 SQL 바인드 파라미터에 비유하는데, 대부분의 비유보다는 잘 들어맞습니다. 중괄호가 있으면 식의 텍스트는 프로시저가 컴파일될 때 고정되고, $qty는 실행 시점에 값으로 들어올 뿐 식의 문법으로 읽히지 않습니다. 중괄호가 없으면 문자열로 조립한 SQL과 같아서, max8님이 센 것처럼 호출마다 다시 파싱됩니다.

    단서가 하나 있습니다. 바인드된 값은 파라미터의 타입을 따릅니다. 중괄호 안의 피연산자는 여전히 expr가 문자열의 생김새에 따라 해석합니다. qty가 0x10이면 expr {$qty + 0}은 16이고, 1e3이면 1000.0입니다. 그러니 중괄호는 주입을 없애지만, 숫자로 읽는 부분에는 kim_dh님이 물은 입력 검사가 여전히 필요합니다.

  9. 고유 링크

    표현 중에서는 '중괄호는 expr에게 적힌 그대로의 식을 건넨다'가 제가 리뷰 댓글에 쓰고 싶은 문장이에요. '중괄호는 expr를 빠르게 한다'는 맞는 말이지만 생략해도 되는 최적화처럼 들리게 해요.

  10. 고유 링크

    PHP에도 create_function이라는 게 있었어요. 문자열 본문을 뒤에서 eval로 돌려서 끼워 넣은 건 뭐든 코드가 됐죠. PHP 8에서 없어졌고, 사람들이 그걸로 하던 일은 클로저가 다 해요.

  11. 고유 링크

    이 얘기를 보고 제 스크립트에서 중괄호 없는 expr를 찾아봤는데, 대부분은 그냥 게으름이었어요. 하나만 아니었어요! 작은 계산기 프로시저가 연산자를 변수에 담아 expr $a $op $b를 하고 있었는데, 여기에 중괄호를 치면 깨져요. expr {$a $op $b}는 문법 오류거든요. 치환은 피연산자만 줄 수 있어요. 마음에 든 해결책은 tcl::mathop 명령 묶음이에요.

    set a 3
    set b 4
    set op +
    puts [tcl::mathop::$op $a $b]

    이건 평범한 명령 호출이라 $a와 $b가 단어로 넘어가고, 두 번 파싱되는 건 없어요. 7이 찍혀요. 더 엄격하게 하려면 허용 목록을 앞에 두고 $op로 switch를 하면 돼요. tcl::mathop::에는 계산기가 굳이 제공하지 않을 **나 << 같은 것도 있으니까요.

  12. 고유 링크

    둘 다요, 순서대로요. 입력 검사가 먼저 와서 그 값이 애초에 수량이 될 준비가 됐는지를 정해요. string is double -strict $qty를 쓰세요. -strict가 없으면 빈 문자열이 통과해서, 빈 수량 칸이 expr까지 가서 덜 친절한 메시지로 실패하거든요. 검사를 통과하면 그다음에 중괄호 친 expr가 합계를 계산해요. 중괄호는 계산을 텍스트로부터 지켜 주고, 검사는 계산이 시작되기 전에 사용자에게 뭐가 잘못됐는지 알려 줘요. tally6님의 0x10 예도 string is double -strict를 통과하니까, 16진수를 수량으로 치면 안 된다면 입력칸 규칙이 하나 더 필요해요.

  13. 고유 링크

    그 단서에는 조금 반대할게요. 16진수나 지수 표기도 숫자니까, 0x10을 16으로 읽는 건 값에 타입이 붙는 거예요. 사용자보다 숫자의 범위를 넓게 볼 뿐이고요. 그건 주입과 같은 종류의 문제가 아니라서 저는 같이 묶고 싶지 않아요. 입력 검사는 폼이 어떤 숫자를 받을지 정해요. 중괄호는 텍스트가 코드가 될 수 있는지를 정해요. 둘을 섞으면 중괄호 없는 버전을 안전하게 만든 게 숫자 검사였다고 믿게 돼요.

같은 질문의 다른 갈래

  1. D의 pure 함수는 인자를 통해 값을 바꿀 수 있다

    D의 pure는 단어가 주는 느낌보다 약해요. 평가기 실험에서 호출을 캐시하려다가 그제야 알아챘어요. D에서 pure는 변경 가능한 전역이나 정적 상태에 접근하지 않는다는 뜻이에요. 매개변수로 닿을 수 있는 메모리를 바꾸는…

    답글 4d
  2. 매크로로는 줄바꿈이 폼을 끝내게 할 수 없다

    매크로로는 줄바꿈이 폼을 끝내게 만들 수 없어요. 자꾸 그렇게 하고 싶어지지만요. 표기는 아주 작아요. 한 줄에 쓴 paint red blue를 (paint red blue)로 읽고, 줄바꿈에서 폼을 닫고, #부터는 주석으로…

  3. 시계를 읽는 규칙은 어떻게 재현하나요?

    가격 결정을 다시 재생해 보려다 못 했어요. 리스펙스로 쓴 규칙 eligible(item)이 호스트 시계와 그때그때 현재인 카탈로그 리비전을 읽고 있어서, 다시 돌리면 다른 세상을 봤거든요. 이제는 요청에 정해 둔 now와…