1P by GN⁺ | ★ favorite | 댓글 1개
  • Zig의 컴파일 타임(comptime) 메타프로그래밍은 런타임 코드처럼 읽히면서도, 구조체 필드 합산 같은 반복 코드를 손으로 작성한 C 함수처럼 효율적인 실행 코드로 만들 수 있음
  • 별도 제네릭 문법 대신 comptime T: typeanytype을 활용해 제네릭 프로그래밍을 처리하는 점이 Zig comptime의 핵심 사용처임
  • 런타임, comptime, 빌드 시스템이 같은 Zig 언어를 공유해, Fizz Buzz 같은 함수도 컴파일 중 실행해 미리 계산된 배열로 만들 수 있음
  • 컴파일러는 가능한 부분을 먼저 평가하고, 런타임 값이 필요한 부분만 나중에 머신 코드가 될 출력 바이트코드로 남김
  • Zig comptime은 텍스트 코드 생성과 비슷한 문제를 풀 수 있지만, C 매크로처럼 키워드를 임의 치환하지 않아 코드 읽기와 디버깅 부담을 줄임

Zig comptime을 읽는 관점

  • Zig의 comptime은 메타프로그래밍 기능이지만, 최종 동작을 읽을 때 컴파일 타임과 런타임의 구분을 크게 의식하지 않아도 됨
  • zig zen의 세 번째 항목인 “Favor reading code over writing code”가 중요한 기준이 됨
    • 매크로나 코드 생성 기반 메타프로그래밍은 원본 코드와 확장된 코드라는 두 층을 만들 수 있음
    • 이런 간접 계층은 코드 읽기, 디버깅, 동작 변경을 어렵게 만듦
  • 배열 [3]i64 = .{1,2,3}의 합을 구하는 런타임 코드와, 구조체 MyStruct의 필드 a, b, c를 합산하는 comptime 코드가 나란히 비교됨
    • inline for (comptime std.meta.fieldNames(MyStruct))로 필드 이름을 순회함
    • @field(my_struct, field_name)으로 컴파일 타임에 알려진 필드 이름을 사용해 필드에 접근함
  • 구조체 필드 합산 예시는 런타임 리플렉션처럼 보이지만, 실행 파일에는 해당 구조체 타입용 합산 함수를 손으로 작성한 것처럼 효율적인 코드가 남음
  • 예시의 변환 과정은 Zig 구현을 그대로 재현한 것이 아니라 개념적 설명에 가까움
  • Zig 코어 팀은 comptime과 런타임이 섞인 코드를 예시처럼 단계 실행할 수 있는 디버거를 작업 중임

제네릭을 comptime으로 처리함

  • Zig에는 별도의 제네릭 기능이 없음
  • 타입을 제네릭으로 만들려면 타입을 받는 함수가 타입을 반환하도록 작성함
    • 예: pub fn GenericMyStruct(comptime T: type) type
    • 반환된 struct의 필드 a, b, c는 모두 T 타입을 사용함
  • 제네릭 함수도 같은 방식으로 작성할 수 있음
    • 예: fn quadratic(comptime T: type, a: T, b: T, c: T, x: T) T
    • quadratic(f32, ...)quadratic(i64, ...)처럼 타입 인자를 명시할 수 있음
  • 인자 타입이 함수 시그니처의 다른 부분에 중요하지 않다면 특수 타입 anytype으로 추론할 수 있음
  • 이 방식만으로 comptime 전체를 설명할 수는 없지만, Zig에서 흔한 제네릭 작업을 이해하는 출발점이 됨

같은 Zig 코드를 컴파일 타임에 실행함

  • Zig는 런타임, comptime, 빌드 시스템에서 같은 언어를 사용함
  • Fizz Buzz 예시는 fizzBuzz(writer) 함수를 먼저 일반 런타임 출력 함수로 작성함
    • 1부터 100까지 순회함
    • 35의 배수에는 fizzbuzz, 3의 배수에는 fizz, 5의 배수에는 buzz, 나머지는 숫자를 출력함
  • 같은 fizzBuzz 함수를 comptime 블록 안에서 실행하면 전체 출력을 미리 계산할 수 있음
    • std.io.countingWriter(std.io.null_writer)로 필요한 바이트 수를 먼저 계산함
    • 계산된 길이로 buffer: [cw.bytes_written]u8 배열을 만듦
    • std.io.fixedBufferStream(&buffer)에 다시 출력해 full_fizzbuzz 값을 만듦
  • 핵심 구간만 측정하면 미리 계산한 버전은 약 9배 빠르게 실행됨
    • 예시 자체가 작아서 전체 실행 시간은 다른 요인에 더 크게 좌우됨
  • comptime과 runtime에는 접근 가능한 값과 기능의 차이가 있음
    • comptime_int, comptime_float, type 값은 comptime에서만 접근 가능함
    • 일부 함수는 comptime 인자만 받아 사실상 comptime 전용이 됨
    • 시스템 호출이나 이를 사용하는 기능은 런타임에서만 접근 가능함
  • 이런 전용 기능을 쓰지 않는 코드는 comptime과 runtime에서 똑같이 동작함

부분 평가로 이해하는 comptime

  • comptime은 컴파일 과정에서 일어나는 부분 평가(partial evaluation) 로 볼 수 있음
  • 부분 평가는 함수에 일부 인자만 주어진 상태에서, 이미 알려진 값만 사용하는 표현식을 먼저 치환·계산하고 남은 미지의 인자만 받는 새 함수를 만드는 방식임
  • 배열 합산 예시는 평가 과정을 단계적으로 보여줌
    • for 루프를 각 반복의 개별 문장으로 분해함
    • array[0], array[1], array[2]를 각각 1, 2, 3으로 치환함
    • sum0 → 1 → 3 → 6으로 갱신함
    • 마지막에는 std.debug.print(..., .{6}) 형태까지 단순화됨
  • 구조체 필드 합산 예시도 같은 방식으로 읽을 수 있음
    • inline for는 comptime에 실행되므로 필드 "a", "b", "c"에 대한 개별 문장으로 펼쳐짐
    • @field(my_struct, "a")my_struct.a로 바뀜
    • 최종 함수는 sum += my_struct.a, sum += my_struct.b, sum += my_struct.c를 직접 가진 형태가 됨
  • 이 방식은 의도를 직접 코드로 표현하면서도, 필드가 바뀔 때 합산 함수를 수동 갱신하라는 주석을 남길 필요를 줄임

컴파일 타임 평가와 런타임 코드 방출

  • Zig comptime은 컴파일 타임 평가와 런타임 코드 방출의 조합으로도 볼 수 있음
  • 입력 코드는 컴파일러에 의해 실행됨
    • 컴파일 타임에 알 수 있는 문장은 바로 평가됨
    • 런타임 값이 필요한 문장은 출력 코드에 추가됨
  • 이 관점은 Zig 컴파일러의 실제 처리 방식에 가장 가까움
    • Zig는 먼저 문법을 파싱함
    • 코드를 가상 머신용 바이트코드로 바꿈
    • VM이 가능한 부분을 평가하고, 런타임 처리가 필요한 부분은 새 바이트코드로 방출함
    • 새 바이트코드는 이후 머신 코드로 변환됨
  • 런타임 입력을 가진 if 같은 조건문은 양쪽 경로를 모두 방출함
  • 죽은 코드는 의미 분석을 받지 않음
    • 잘못된 함수를 작성해도 실제로 사용하기 전까지 컴파일 오류가 나지 않을 수 있음
    • 이 특성은 컴파일을 더 효율적으로 만들고, #ifdef 없이 자연스러운 조건부 컴파일을 가능하게 함
  • Zig의 타입 이름도 comptime에서 type 값을 평가하는 표현식임
    • 함수 인자 타입처럼 단순해 보이는 타입 이름도 comptime 평가 결과임
    • 이 특성이 앞선 제네릭 예시를 가능하게 함
    • 필요하면 더 복잡한 표현식으로 타입을 계산할 수 있음
  • Zig의 정적 분석은 다른 많은 정적 타입 언어보다 복잡함
    • 모든 타입을 알아내려면 컴파일러의 상당 부분이 실행되어야 함
    • 도구가 따라잡기 전까지 코드 완성 같은 편집기 기능이 항상 잘 동작하지는 않음

텍스트 코드 생성과의 대응 관계

  • Zig comptime은 텍스트 코드 생성과 형태는 다르지만, 비슷한 문제를 풀 수 있음
  • 코드 생성 함수 writeSumFnwriter.printsumFields 함수의 소스 코드를 출력함
    • 생성기에서 실행되는 코드는 Zig comptime 부분에 해당함
    • 생성기가 출력하는 코드는 런타임 코드에 해당함
  • Zig의 comptime 버전은 같은 로직을 더 직접적으로 표현함
    • std.meta.fieldNames(MyStruct)로 필드 이름을 얻음
    • inline for@field로 필드 접근 코드를 구성함
  • 타입 정보를 입력으로 쓰는 코드 생성은 Zig에서 더 단순해짐
    • 텍스트 생성 방식은 타입 이름과 필드 이름 정보를 어디서 가져오는지 별도로 해결해야 함
    • Zig는 타입 자체와 필드 정보를 comptime에서 직접 다룰 수 있음
  • 외부 명세 같은 입력을 쓰는 경우에는 @embedFile로 파일 내용을 포함한 뒤 평소처럼 파싱할 수 있음
  • 제네릭 구조체 예시에서는 comptime이 텍스트 코드 생성보다 타입 이름 참조를 더 직접적으로 처리함
    • 텍스트 생성은 MyStruct_{s} 같은 이름을 문자열로 이어붙여 일관되게 맞춰야 함
    • comptime은 GenericMyStruct(T)처럼 함수를 직접 사용함
  • 예외도 있음
    • 필드 이름이 comptime에 결정되는 타입을 만들 수는 있음
    • 이 경우 필드 정의 목록을 담은 명세로 내장 함수를 호출해야 함
    • 그런 타입에는 메서드 같은 선언을 정의할 수 없음
    • 코드 표현력 자체를 제한하지는 않지만, 다른 코드에 노출할 수 있는 API 형태는 제한함
  • C 같은 텍스트 매크로로 가능한 합리적인 작업 대부분은 comptime으로도 가능하지만, 코드의 모양은 거의 다를 수 있음
  • Zig는 텍스트 매크로처럼 키워드를 다른 이름으로 치환하는 기능은 제공하지 않음

더 읽을거리와 표준 라이브러리 예시

  • Zig 자체는 comptime 하나에만 의존하는 언어가 아니며, 공식 웹사이트에서 더 배울 수 있음
  • 언어 레퍼런스는 comptime의 구체 기능을 다룸
  • Zig 표준 라이브러리에는 comptime 사용 예시가 있음
    • std.debug.print가 사용하는 포맷 함수는 강력한 제네릭 함수임
      • 많은 언어는 포맷 문자열을 런타임에 파싱하고, 조기 오류 검출을 위해 별도 검증기를 추가할 수 있음
      • Zig는 포맷 문자열을 comptime에 파싱해 효율적인 출력 코드를 만들고 모든 검증도 컴파일 타임에 수행함
    • ArrayList는 비교적 단순하지만 완전한 기능을 가진 제네릭 컨테이너임
    • Zig의 main 함수는 여러 반환 타입을 가질 수 있고, 이는 컴파일러의 마법이 아니라 일반적인 comptime 코드로 처리됨

댓글과 토론

Hacker News 의견들
  • 컴파일 시점 프로그래밍에서 이미 발견된 문제들을 무비판적으로 칭찬하기보다 더 깊게 다뤘으면 좋겠음
    단계적 프로그래밍은 새롭지 않고, Lisp 계열에서도 수십 년간 비슷한 일을 해왔지만 그 과정에서 많은 설계 절충과 문제가 드러났음. 예를 들어 이런 방식의 제네릭은 매개변수성(parametricity) 을 깨뜨려, 함수의 타입 시그니처만 보고 추론하기 어려워짐. 재귀 제네릭 타입을 Zig가 어떻게 다루는지도 불명확하고, 타입 검사와 컴파일 시점 계산이 언제 어떤 순서로 상호작용하는지도 중요한 선택지임. 글에서는 컴파일 시점 코드가 값뿐 아니라 코드를 생성할 수 있다고 하지만 위생성(hygiene)은 다루지 않음. 관련 논의로 https://typesanitizer.com/blog/zig-generics.html가 좋음

    • Zig를 꽤 좋아하고 몇 년간 지켜보며 가끔 써왔지만, comptime은 제네릭, 복잡한 자료구조의 컴파일 시점 초기화, 대상별 코드 생성 같은 경우에 특히 빛남
      다만 다른 상황에서는 Zig 코드에서 comptime을 보면 “아…” 싶을 때가 있음. Lisp 매크로처럼, 사실 없거나 코드 구조를 더 잘 잡으면 사라질 문제를 피하려고 쓰기 쉬움. 원 글의 구조체 필드를 순회해 값을 더하는 예시는 실제로 사람들이 comptime을 쓰는 방식의 전형인데, 많은 경우 std.enums.EnumArray처럼 실제로 순회 가능한 자료구조를 쓰는 편이 더 나음
    • 매개변수성이 깨진다는 게 실무 프로그래머가 함수의 동작을 이해하는 차원인지, 타입 람다 계산 같은 타입 이론 체계에서 추론하는 차원인지 궁금함
      이론적으로는 우려가 분명 있지만, 실용 언어에서 얼마나 중요한지는 별개임. C++ 템플릿도 템플릿 특수화를 통해 매개변수성을 깨지만, 실무에서는 큰 문제가 되지 않는 편이고 오히려 최적화를 가능하게 함. 예를 들어 std::vector<bool>처럼 각 원소를 실제로 1비트로 저장하는 구현은 이런 유연성이 있어야 가능함
    • 글쓴이로서, 이 글은 모임에서 설명하려던 내용을 잘 말하지 못한 뒤 정리하려고 쓴 것이어서 비판보다는 설명에 초점을 뒀음
      Zig식 comptime은 더 정적으로 정의된 함수 시그니처와 비교해 추론 가능성 대 표현력의 직접적인 절충이라고 봄. 타입으로 그런 추론을 하는 코드에만 영향을 주며, 필요에 따라 선택할 수 있는 부분임. 타입 시그니처로 충분하지 않을 때도 소스 몇십 줄을 읽으면 사용법이 대체로 풀렸고, 오래 헷갈린 적은 없음. 재귀 제네릭 타입 예시는 선언 중인 이름을 쓰면 “dependency loop detected”가 나지만 우회 방법이 있고, 글의 제네릭 예시도 자기 자신을 참조함. 타입 검사는 comptime 도중 일어나며, 예를 들어 첫 @compileLog("Hi")는 실행되지만 그 뒤 const a: u32 = "42";에서 타입 오류가 나면 두 번째 로그까지 가지 않음. comptime과 런타임 사이의 타입 검사에서 미묘한 문제가 생길 수는 있지만 꽤 난해한 코드에서나 보이고 쉽게 해결되며, 핵심 팀도 알고 있어 1.0 전에 다뤄질 것으로 기대함. 위생성이 정확히 무엇을 뜻하는지는 더 설명이 필요함
    • 함수 선언만 보고 추론할 수 있다는 건 컴파일 시점 실행과는 별개임
      함수가 명확한 논리적 목적을 갖고 이름과 매개변수 이름이 잘 붙어 있으면 선언만으로도 추론할 수 있음. 매개변수의 라벨, 타입 이름 등 프로그래머가 지정할 수 있는 부분도 이름의 일부로 볼 수 있음. 링크한 글은 제목의 결론에는 동의하지만, 의미 있는 논증보다는 점수를 따려는 토론팀식 글에 가까움. 더 나은 구도는 유연성 대 복잡성임. 고정된 제네릭 시스템은 잘 설계되면 프로그래밍 가능한 것보다 단순하지만 덜 유연함. Zig의 장점은 메타프로그래밍 언어가 일반 언어와 거의 같고, 그 일반 언어 자체도 단순해서 추가 복잡도 비용을 줄인다는 데 있음. 다만 컴파일 시점 코드와 런타임 코드가 섞이고 단서가 적어, 프로그래머가 무엇이 언제 실행되는지 구분하기 어려워지는 복잡도는 생김. 언어 서버나 편집기 플러그인이 comptime 코드에 다른 배경색을 주는 “comptime 셰이더”를 제공하면 어떨까 싶음
    • 이 주제가 15분 데모에 기반해 논의되고 실제 절충에 대한 이해가 부족한 게 지침. 링크한 Varun Gandhi의 글은 훌륭함
      Rust 경험상 사람들이 상수 제네릭으로 하려는 많은 일은 comptime 같은 기능이 있으면 더 쉬울 것 같음. 매개변수성을 유지하면서 상수 제네릭에 대한 산술을 허용하는 건 구현이 어렵고, 실제로 원하는 게 “출력 크기가 N인 해시 함수 트레이트” 정도라면 그 목적을 위해 매개변수성을 포기하고 N에서 트레이트를 이전 코드 생성 단계로 만드는 것도 괜찮을 수 있음. 하지만 Rust 매크로는 그런 식으로 쓰기엔 너무 유연하고 귀찮음. 그래도 매개변수 다형성을 순진한 코드 생성 기능으로 대체하는 순간 고생길이 열림
  • D에는 17년 전부터 있었음. D 기능은 꾸준히 다른 언어로 옮겨감
    Zig는 comptime 키워드로 컴파일 중 실행될 블록을 표시하지만, D는 키워드가 아니라 상수 표현식인지가 실행을 결정함. 예를 들어 int s = sum(3, 4);는 런타임에 실행되고 enum e = sum(3, 4);는 컴파일 시점에 실행됨. 비상수 전역, 입출력, malloc() 같은 시스템 함수 호출을 피하면 상당수 함수가 변경 없이 컴파일 시점에 실행될 수 있음. D의 자동 메모리 관리 덕분에 메모리 할당도 가능함

    • 좋아하는 활용 중 하나는 정적 테이블 생성
      예전에는 정적 테이블을 만들기 위해 별도 프로그램을 작성했지만, 컴파일 시점 함수 실행으로 그럴 필요가 없어졌음. 배열 tytab의 초기화 값은 배열을 계산해 반환하는 람다에서 나옴. 전체 예시는 https://github.com/dlang/dmd/blob/master/compiler/src/dmd/ba...에 있음. CTFE의 또 다른 흔한 용도는 DSL을 만드는 것임
    • D를 만든 것에 감사하다는 말을 할 기회는 놓치고 싶지 않음. 다만 컴파일 시점 실행의 한계가 궁금함
      컴파일러가 도는 머신과 프로그램이 실행될 머신이 다르면 D 컴파일러는 어떻게 올바름을 보장하는가? 예를 들어 int s = sum(100000, 1000000)이 모든 x86 머신에서 같은 값이라는 걸 컴파일러가 어떻게 아는지 궁금함. CPU 세대별로 미묘한 차이가 있을 수 있는데, 실제로 호스트에서 한 계산이 대상 머신에서도 같은 결과가 된다고 어떻게 보장하는지, 아니면 아키텍처가 같으면 호스트와 대상이 충분히 비슷하다고 가정하는지 궁금함
    • D의 ImportC도 C 코드로 CTFE를 할 수 있음
      int sum(int a, int b) { return a + b; }를 두고 _Static_assert(sum(3, 4) == 7, "look ma, check at compile time!");처럼 쓸 수 있음. C 표준은 왜 이걸 추가하지 않는지 모르겠고, 잘 동작함
    • 공평하게 말하면 Zig도 같은 함수를 런타임 문맥과 comptime 문맥에서 모두 호출할 수 있음
      square(2)는 런타임 호출이고 comptime square(3)은 컴파일 시점 호출임. comptime 호출은 무언가 컴파일 시점 호환이 아니면 컴파일 오류를 내는데, 이는 중요한 기능이라고 봄. 컴파일 시점에 실행될 것으로 기대한 코드가 입력 인자 변화 때문에 우연히 런타임 평가로 밀려나는 경우 경고 신호가 되기 때문임
  • Zig는 흥미로워 보이지만 연산자 오버로딩이 있었으면 좋겠음
    연산자 오버로딩 반대 논거 대부분은 잘 납득되지 않음. 내부에서 실제로 무슨 일이 일어나는지 모른다는 주장은 add라는 함수가 곱셈을 하도록 만들 수도 있으니 충분하지 않음. C++의 iostreamsboost::spirit이 남용 사례로 자주 나오지만, 연산자 오버로딩이 있는 다른 언어들에서는 그런 일이 별로 보이지 않고 C++ 특유의 문제처럼 보임

    • OCaml식 해법이 Zig의 용도에 잘 맞을 것 같음
      OCaml에서는 연산자를 재정의할 수 있지만 다른 모듈의 문맥 안에서만 가능함. 예를 들어 Vec3 모듈에서 +를 재정의했다면 Vec3.(a + b + c + d)let open Vec3 in a + b + c + d처럼 쓸 수 있음. 소스를 읽을 때 “이 +는 어디서 온 거지?”라는 의문이 없고, a.add(b).add(c).add(d)보다 훨씬 낫다. 다만 Zig는 이미 굳어지는 중이라 이런 문제를 해결하는 큰 변화가 들어가긴 어려워 보임
    • Python과 PHP의 __ 함수들 뒤에서 얼마나 많은 마법이 도는지 모를 수 있음
      Zig의 접근은 신선하고, 코드를 따라갈 수 있는 능력이 몇 글자 더 입력하는 데 드는 몇 초보다 중요함
    • 연산자 오버로딩을 싫어하는 이유 상당수는 예상 밖의 런타임 성능과 관련 있다고 봄
      이상적인 해법은 오버로딩임을 명확히 드러내는 사용자 정의 연산자를 언어가 제공하는 것임. 예를 들어 let c = a |+| b 같은 접두/접미 형태라면, 코드를 보는 사람이 |+| 연산이 실제로 함수 호출임을 알 수 있음. 여전히 남용될 수는 있지만 주요 우려 중 하나는 줄일 수 있음
    • C++에서는 연산자 오버로딩을 벡터/행렬 수학에만 썼고, 그 경우에는 정말 유용했음
      언어가 셰이딩 언어처럼 벡터 수학 문법을 직접 제공한다면 괜찮을 것 같음. Zig에는 적어도 Clang의 Vector Extension과 조금 비슷한 @Vector() 타입이 있지만, 아쉽게도 Extended Vector Extension은 아님. https://ziglang.org/documentation/master/#Vectorhttps://clang.llvm.org/docs/LanguageExtensions.html#vectors-... 참고
    • 숫자 배열에 대한 기본 선형대수 연산자는 오버로딩 대신 언어에 그냥 내장되어야 할지도 모름
      그런 제안이 이미 있는지는 잘 모르겠음
  • fieldNames는 Nim의 fieldPairs와 아주 비슷해 보이고, 엄청나게 편리한 구조임
    효율적인 직렬화를 매우 쉽게 만들고, 최근에는 fieldPairs를 써서 타입에 대한 스레드 안전성 컴파일 시점 검사를 20줄 정도로 구현했음. 이런 기능은 프로그래밍 언어의 표준 기능이 되어야 한다고 봄. Rust에서 가장 아쉬운 점 중 하나가 여기에 있고, 마지막으로 시도했을 때는 타입 없는 매크로로 제한되어 있었음. 너무 제약이 큼. 크레이트의 구조체에 serde가 구현되어 있길 바랄 수밖에 없고, 같은 필드를 가진 구조체를 프로그램적으로 직접 만들 수도 없음

    • 한때 컴파일 시점 리플렉션 논의가 있었고, 아마 이런 기능도 포함할 수 있었겠지만 관련된 드라마와 함께 사라진 것 같음
      꽤 아쉬움. serde 같은 것들이 컴파일 시점 리플렉션이 있으면 훨씬 구현하기 쉬웠을 것임
    • 컴파일 시점 리플렉션이 있으면 ORM이나 웹 프레임워크 같은 프레임워크를 만들 수 있음
      유일한 절충은 그런 라이브러리를 소스 코드 형태로 포함해야 한다는 것임
  • 어느 정도 완성된 C 파서 라이브러리를 작성해본 뒤로, 언어 자체에 메타프로그래밍이 꼭 필요하다는 주장은 잘 모르겠음
    구조체, 직렬화, 프로퍼티, 계측 등을 생성하고 싶으면 그냥 소스 파일을 처리해 소스 파일을 출력하는 일반 C 프로그램을 작성하고, 빌드 스크립트에서 먼저 실행하면 됨. 이런 메타 프로그램들은 어떻게 디버그하고 테스트하는지 궁금함. 내 것은 일반 C 프로그램이라 다른 코드와 같은 디버거와 도구를 그대로 씀

    • 바로 그런 일을 사람들이 하기 싫어하는 것임
    • C#은 더 정확히는 Roslyn/dotnet이 이걸 꽤 잘 제공함
      컴파일러 자체가 같은 언어로 작성되어 있어서, AST에 읽기 전용으로 접근하고 C# 소스를 내보내는 플러그인을 넣을 수 있음. 디버깅은 좋은 테스트 프레임워크를 만들기 위해 약간 더 작업해야 하지만, 그 뒤에는 표준 단위 테스트 프레임워크 안에서 플러그인을 붙인 컴파일러를 실행하고 대화형 디버거로 볼 수 있음
    • Ryan Fleury 등이 옹호하는 접근과 같고, 충분히 좋은 방식임
      “C에서 임의의 컴파일 시점 실행: cl /nologo /Zi metaprogram.c && metaprogram.exe, cl /nologo /Zi program.c. 컴파일 시점 코드는 네이티브 속도로 돌고 디버그 가능하며 완전히 절차적이고 임의적이다. 컴파일러가 코드를 실행해줄 필요는 없다.” https://x.com/ryanjfleury/status/1875824288487571873
    • Zig는 잘 모르겠지만 Lisp의 힘은 S-표현식, 다시 말해 AST를 조작한다는 데 있음
      C에서 그렇게 하려면 소스 파일을 처리하는 C 프로그램을 위해 완전한 C 파서를 작성해야 함
    • 예전에 Python에서 numba JIT와 함께 비슷한 일을 했음
      Python 코드를 생성하는 Python 코드를 쓰고, 그걸 다시 컴파일하게 했음. 이건 깨지기 쉽고 끔찍한 난장판이었고, 이런 일을 해야 했던 게 Python을 떠난 큰 이유 중 하나였음. 모든 함수 인자를 문자열로 넘기면 되지 않느냐고 묻는 것과 비슷함. 문자열 기반 코드를 쓰는 사람도 있지만 드물어야 하고, 언어는 피할 수단을 제공해야 함
  • 흥미로운 패턴 하나는 컴파일 시점에 구조체를 생성할 수 있다는 것임
    PyTorch에서 JSON 파일을 만들고, Zig의 @embedFile로 읽은 뒤, 특정 run 메서드를 가진 구조체를 생성해 신경망을 구현하는 실험을 해봤음. 이론적으로는 컴파일러가 신경망을 직접 최적화할 수 있게 해줌. 큰 이점은 아직 증명하지 못했지만, 전체 네트워크가 스택에 있어 동적 할당이 없다는 점도 있음. 이게 좋은지는 확실치 않음

    • 이런 일은 comptime 대신 Python으로 코드 생성기를 작성해서 해봤음
      comptime Zig가 특별히 빠를지 확신이 없고, 구조체를 생성하는 JSON 파서를 매번 돌리고 싶지도 않음
    • 컴파일 시간에는 어떤 영향을 주는지 궁금함
  • 흥미롭긴 하지만, 이런 경우에는 “D가 먼저 했다”는 식의 댓글에 동의하게 됨

    • 맞고, 위생적으로 처리되며 전처리기 방식도 아님
  • Zig의 comptime에 놀랐다면, 컴파일 시점 코드 평가와 전체 AST 매크로 시스템을 갖춘 Nim도 꼭 살펴볼 만함

    • Nim은 재미있는 언어지만 “진지한” 작업에는 고려하지 않을 것 같음
      다른 틈새 언어와 같은 생태계 문제가 있고, 거기에 호불호가 갈리는 메인터이너, 오래 남지 않는 듯한 핵심 기여자들, 암호화폐 회사의 주된 자금 지원 같은 요소도 있음. 10년 전이라면 이런 것들이 거슬리지 않았겠지만 지금은 다름
    • Zig에는 예외가 없다는 장점이 있음
      Nim도 예외에서 벗어나려는 것 같지만, 예외는 함수를 물들이고 예외를 던지는 함수를 쓰지 않더라도 신경 써야 하게 만듦[1]. 보이지 않는 제어 흐름을 다루기엔 인생이 너무 짧음. [1]: https://github.com/status-im/nim-stew
  • Zig는 전반적으로 꽤 좋은 언어이고 필요한 일을 함
    목적에서 벗어나지 않는 것이 매우 중요하며, 그래서 몇몇 언어가 그냥 쓰인다는 이유만으로 아무 데나 쓰이는 것을 별로 좋아하지 않음

  • Zig의 메타프로그래밍 능력과 Rust의 거대한 생태계, 커뮤니티, 안전성을 결합한 무언가가 있었으면 좋겠음
    언어 설계만 보면 Rust보다 Zig가 훨씬 마음에 들지만, 서툰 취미 프로그래머로서는 적어도 지금은 Zig로 실제로 유용하거나 신뢰할 만한 무언가를 작성할 자신이 없음

    • Rust에 익숙한 입장에서 Zig를 잠깐 시도했다가 금방 포기했음
      컴파일러가 그런 문제들을 컴파일 시점에 찾아주는 데 도움을 주지 않았기 때문임. Zig가 그런 약속을 하지 않는다는 건 알지만, 내게는 결정적인 단점이라 Zig는 맞는 언어가 아닌 것 같음. 반면 comptime 개념은 Rust 매크로보다 마음에 듦
    • Rust 커뮤니티는 Zig에서 멀리 떨어져 있었으면 함. 농담이지만, 대체로는 진심임