1P by GN⁺ | ★ favorite | 댓글 1개
  • 1989년 FM TOWNS용 High C Compiler는 DOS 환경 대응을 넘어, 당시 C 컴파일러로는 드문 사용자 지향 언어 기능을 여럿 담고 있었음
  • Phar Lap의 DOS extender와 결합되며 16비트 MS-DOS 환경에서 32비트 80386을 활용하는 개발 흐름 속에 FM TOWNS의 1st-party C 컴파일러가 됨
  • 숫자 리터럴 밑줄, 라벨 인자, case 범위, 중첩 함수, 제너레이터 등은 C/C++ 표준에 훨씬 늦게 들어갔거나 아직도 표준에 없는 기능임
  • 중첩 함수는 함수 포인터와 컨텍스트 포인터를 함께 전달하는 비탈출 클로저 형태의 “full function value”를 제공해 일반 C 함수 포인터보다 표현력이 컸음
  • 제너레이터는 중첩 함수 위의 문법 설탕으로 구현되어, 호출자의 for 루프 본문을 중첩 함수로 바꿔 yield 인자로 넘기는 단순한 구조로 동작함

FM TOWNS와 High C의 자리

  • FM TOWNS 관련 책 더미에서 나온 1980년대 C 컴파일러 매뉴얼이 예상보다 풍부한 언어 확장을 담고 있었음
  • C와 그 계열 언어를 실제 환경에서 쓰려면 오랫동안 벤더 확장이 필요했음
    • 오늘날 GCC, Clang, MSVC 중심 환경에서는 플랫폼별 처리나 저수준 세부 제어에 확장이 집중되는 경향이 있음
    • 1980년대에는 더 작고 많은 회사들이 채택 경쟁을 벌이면서 확장 기능도 더 다양했음
  • Phar Lap은 16비트 MS-DOS 환경에서 32비트 80386 프로세서를 활용하게 해주는 초기 DOS extender 중 하나를 만들었음
  • MetaWare는 Phar Lap의 의뢰로 High C Compiler를 Phar Lap의 DOS extender SDK에 포팅함
  • Fujitsu는 803386 기반 FM TOWNS 플랫폼 OS에 Phar Lap의 DOS extender를 통합했고, High C는 이 플랫폼의 1st-party C 컴파일러가 됨
  • FM TOWNS는 첫 ANSI C 표준인 C89가 비준되기 직전인 1989년에 출시됨

표준보다 앞섰던 작은 편의 기능들

  • 숫자 리터럴의 밑줄 구분자

    • 긴 숫자 리터럴을 읽기 쉽게 만들기 위해 숫자 안에 밑줄 구분자를 넣을 수 있었음
    • C++은 C++14에서 1'000'000 같은 작은따옴표 구분자를 도입함
    • C는 C23에서야 유사한 기능을 도입함
  • 라벨 인자

    • 매개변수가 많거나 bool처럼 호출 지점에서 의미가 드러나기 어려운 타입을 많이 쓰는 함수에서 인자 이름을 붙일 수 있음
    • High C의 라벨 인자는 Python의 인기 기능과 비슷하게 동작함
      • 인자 라벨은 선택 사항임
      • 라벨이 있으면 argumentName => value 문법으로 인자를 임의 순서로 지정할 수 있음
      • 라벨 없는 인자와 라벨 있는 인자를 섞을 수 있지만, 함수의 모든 매개변수에는 대응하는 인자가 있어야 함
    • 표준 C와 C++에는 아직 이 기능이 없음
  • case 범위

    • Pascal의 case low..high처럼 값의 범위를 한 번에 매칭하는 기능을 제공함
    • 표준 C와 C++은 이 기능을 채택하지 않았음

중첩 함수와 full function value

  • High C는 Pascal처럼 함수 안에 중첩 함수를 선언할 수 있음
  • 구현 방식은 표준 Pascal이나 GCC의 중첩 함수 확장보다 더 완전한 형태에 가까움
  • High C는 중첩 함수 선언뿐 아니라 full function value 타입도 선언할 수 있음
    • 전통적인 C 함수 포인터와 달리 함수 포인터에 컨텍스트 포인터를 함께 담음
    • 중첩 함수가 캡처한 컨텍스트를 다시 찾을 수 있게 해줌
    • 바깥 함수가 반환된 뒤까지 수명이 이어지지는 않는 비탈출 클로저임
  • GCC의 중첩 함수 확장은 중첩 함수를 일반 함수 포인터로 참조하게 하려고 호출 스택에 실행 가능한 코드를 써서 컨텍스트 포인터를 thunk했음
    • 이 방식은 보안상 큰 위험으로 이어졌고, 많은 플랫폼이 해당 기능을 완전히 비활성화하게 됨
  • High C의 로컬 함수 참조는 1급 값처럼 사용할 수 있지만, 바깥 함수 반환 이후까지 수명이 연장되지는 않음
  • 중첩 함수는 부모 함수로 goto할 수도 있음
    • Smalltalk 블록처럼 중첩 함수 밖으로 빠져나가는 비지역 탈출이 가능함
    • 이를 이용해 제어 흐름처럼 동작하는 함수를 만들 수 있음
  • Objective-C는 2009년에 escaping closure로 사용할 수 있는 blocks를 얻었고, C++은 2011년에 lambdas를 도입함
  • 두 기능 모두 비지역 탈출 능력은 갖지 않았음
  • 표준 C에는 아직 공식 중첩 함수 기능이 없음

제너레이터 코루틴

  • MetaWare는 한 장 전체를 할애할 정도로 제너레이터 기능을 강조함
  • High C는 1989년에 plain C에서 Python 스타일 제너레이터 코루틴을 지원했음
  • 제너레이터 함수는 void foo(Arg arguments) -> (Yield yields) 문법으로 선언함
    • 함수 내부에서 마법 함수 yield(values...)를 여러 번 호출해 값의 시퀀스를 생성할 수 있음
    • 호출자는 for variable... <- foo(arguments...) do { ... } 형태의 새 for 루프 문법으로 생성된 값을 차례대로 순회함
  • 이 구현은 중첩 함수와 복잡하게 조합될 수 있음
    • 제너레이터 안의 중첩 함수는 바깥 제너레이터의 yield 동작을 캡처할 수 있음
    • 중첩 함수가 자신을 재귀 호출해 트리나 재귀적 데이터 구조를 순회하면서 각 단계에서 yield할 수 있음
  • 이런 형태는 Python이나 많은 주류 제너레이터 코루틴 언어에서는 구현하기 어려운 방식으로 보임

제너레이터 구현 방식과 표준 언어와의 차이

  • High C의 제너레이터는 고급 런타임 없이 중첩 함수 위의 문법 설탕으로 동작함
  • void foo(Arg arguments) -> (Yield yields) 형태의 제너레이터 선언은 일반 함수 void foo(void yield(Yield yields)!, Arg arguments) 선언과 동등함
    • yield는 “full function value” 타입의 암시적 매개변수임
    • 제너레이터 본문 안의 yield(values) 호출은 이 암시적 함수 매개변수를 호출하는 일반 함수 호출임
  • 호출자 쪽의 for 루프 본문은 중첩 함수로 변환됨
    • 이 중첩 함수가 제너레이터의 yield 인자로 전달됨
    • 구조는 단순하지만 효과적임
  • 중첩 함수가 비지역 탈출을 지원하므로 for 루프 본문 밖으로 나가는 break, continue, goto도 적절한 루프 바깥 위치로 goto하는 방식으로 동작함
  • 표준 C가 이런 기능을 통합하려 할 가능성은 낮음
  • C++20은 컴파일 타임 코루틴 변환에 기반한 매우 유연하고 복잡한 코루틴 기능을 제공함
    • 이를 사용해 제너레이터를 구현할 수는 있을 것으로 보임
    • 다만 그 결과물은 로컬 함수와 이렇게 직관적으로 조합되지는 않을 것으로 보임

댓글과 토론

Hacker News 의견들
  • 2011년에 이터레이터 기반 for를 정리해 둔 적이 있음. 이미 오래전에 잊힌 기능 중 하나였고, 당시 C++ 표준에 들어간다면 어떤 모습일지도 함께 다뤘음
    운 좋게도 영어판 High C/C++ Language Reference를 한 권 가지고 있음
    http://jdebp.uk./FGA/metaware-iterator-driven-for.html
    http://jdebp.uk./Proposals/metaware-iterator-driven-for.html
    • breakreturn이 어떻게 컴파일되는지 궁금함. yield 함수가 상태 코드를 반환하도록 변환되고 호출 지점에서 검사되는 방식이어야 했을까?
    • 그거 뒤집힌 웃는 얼굴을 의도한 건가?
  • D에는, Das BetterC에서도, 이런 기능들이 있음: 숫자 리터럴의 밑줄, case 범위, 이름 있는 인자, 중첩 함수, 정적 중첩 함수, 생성기와 비슷한 기능
    예를 들면 int a = 1_234_567;, case 5 .. case 6:, test(b:3, a:4); 같은 형태가 가능함
    정적 중첩 함수는 바깥 함수의 프레임 변수에 접근할 수 없어서 Error: static function test.foo.plus cannot access variable i in frame of function test.foo 같은 오류가 남
    생성기와 비슷한 기능은 https://dlang.org/spec/statement.html#foreach_over_struct_an...에 있음
    • 이 글을 읽는 내내 D가 떠올랐음. 댓글에 Walter Bright가 나타날 것 같다는 생각이 듦
    • D의 가비지 컬렉터도 정말 좋은 기능이라고 봄. 저수준 코드에서는 수동 메모리 관리가 필요할 때가 있지만, 실제로는 별로 상관없고 가비지 컬렉터가 일을 훨씬 쉽게 만들어 주는 부분도 많음
      예를 들어 인메모리 캐시 서비스를 만든다면 캐시 항목 자체는 가비지 컬렉터가 추적하지 않는 편이 좋음. 가비지 컬렉터는 실제 접근 패턴을 모르는 경우가 많아서 방해가 되기 때문임. 하지만 그 서비스의 다른 구성 요소 대부분은 가비지 컬렉터가 있는 편이 더 잘 맞음
    • 질문이 있음. 사람들이 왜 C에서 중첩 함수라는 개념을 싫어하는지 아는가?
      이름 있는 인자는 왜 test(a:4, b:3) 형태이고 test(.a=4, b.=3);가 아닌가?
      C에서 일급 타입을 어떻게 처리할 수 있을지도 궁금함
  • 관련해서 lcc-win C 컴파일러는 연산자 오버로딩, 기본 함수 인자, 함수 오버로딩을 추가했음. 문서에서는 “generic functions”를 보면 됨 [1]
    Plan 9 C 컴파일러도 여러 언어 확장을 도입했고, 그중 익명 구조체/공용체 같은 일부는 나중에 C 표준에 들어감. 현재 GCC는 -fplan9-extensions 플래그를 받아들이며 [2], 함수 호출과 대입에서 구조체 포인터를 익명 필드로 자동 변환하는 등 꽤 유용한 기능을 켤 수 있음
    [1] https://lcc-win32.services.net/C-Tutorial.pdf
    [2] https://gcc.gnu.org/onlinedocs/gcc/Unnamed-Fields.html
  • 이런 기능들을 만든 천재가 누구였을까? 그 회사 안에 굉장히 선견지명 있는 사람이 있었던 듯함
    세상에 널리 퍼져 언어 표준에 영향을 주지 못한 게 아쉬움. 그렇게 오래전에 이런 기능이 있었다는 게 놀라움
    Hacker News에서도 예전에 다뤄졌음: https://news.ycombinator.com/item?id=38938402
    어딘가에 PDF 사본이 있을까?
    • CLU는 1970년대 중후반에 이미 이터레이터, 즉 생성기와 yield가 있는 for 루프를 갖고 있었음 [0]. 비슷한 시기의 Icon 언어도 유사한 생성기 기능이 있었고 [1], yieldsuspend라고 썼음. Ada(1983)에도 이런 기능이 있었던 것으로 앎
      이런 언어 기능들이 완전히 알려지지 않았던 건 아님
      [0] https://publications.csail.mit.edu/lcs/pubs/pdf/MIT-LCS-TR-2...
      [1] https://dl.acm.org/doi/pdf/10.1145/800055.802034
    • Bitsavers에 HC 1.2 참조 매뉴얼(1985) 사본이 있음
      숫자 안의 밑줄, case 범위, 이름 있는 매개변수, 중첩 함수, 완전한 함수 변수까지 설명함
      https://bitsavers.org/pdf/metaware/…
      파일 끝에서 50여 쪽쯤 앞의 Appendix A를 보면 됨
    • MetaWare는 80~90년대 Santa Cruz에 있던 다작 컴파일러 회사였음. 그들이 만든 것을 좋아했고, 문화도 꽤 흥미로웠음
      예전에 코드를 배우고 쓰던 시절에는 살짝 수상한 사이트들을 통해 알게 됐음
    • 그리 놀랄 일은 아님. FORTRAN, Lisp, ALGOL, COBOL 이후의 고급 프로그래밍 언어 아카이브를 파고들면 이런 언어 아이디어가 많이 보임
      시스템 프로그래밍 언어의 풍부한 역사도 발견하게 됨. C와 Go의 설계가 다른 생태계에서 진행되던 일과 과거 경험을 무시했다는 점에서 얼마나 비슷한지도 보임
    • 이런 기능들이 대부분의 프로그래밍 언어가 제공하는 표준 기능 목록의 일부가 아니라 새로운 기능처럼 보이는 건 아쉬움
      컴파일러 매뉴얼 링크는 https://winworldpc.com/product/metaware-high-c-cpp/33x에 있음
      C 매뉴얼 PDF에는 2007년 저작권 표기가 있음
  • 이전 제출과 댓글은 여기 있음: https://news.ycombinator.com/item?id=38938402
  • 그림 속 예제의 문자열 리터럴이 \n이 아니라 ¥n으로 끝나는 이유가 궁금하다면, 이 코드 예제들이 Shift-JIS로 작성된 것 같음. Shift-JIS에서는 ASCII의 \ 위치에 ¥가 들어감
    • 원래는 1969년의 일본어 ASCII 변형인 JIS Roman [0]이었음. Shift-JIS는 훨씬 나중에 이중 바이트 문자 집합 지원을 추가한 것임
      [0] https://en.wikipedia.org/wiki/JIS_X_0201
    • 문제는 Shift-JIS에서 백슬래시의 ASCII 코드가 2바이트 문자의 두 번째 바이트로도 쓰인다는 점임. 그래서 C에서 일본어 문자열 리터럴이 제대로 동작하지 않을 때가 있음
      이 용도에는 EUC-JP가 더 나음. 그런 문제가 없기 때문임. Pascal에서 (* *) 주석을 쓰고 { } 주석을 쓰지 않는다면 Shift-JIS를 써도 이 문제는 없음
    • 저자가 이 책이 언제 나왔는지 정보를 주지 않았고, 찾아도 정보가 없음. 하지만 책이 나왔을 때는 Shift-JIS 표준이 아직 없었을 것 같음
      대신 Shift-JIS의 기반이 된 JIS X 0201(https://en.m.wikipedia.org.org/wiki/JIS_X_0201)이 쓰였을 가능성이 큼
    • 비슷하게 일본어 DOS 프롬프트는 C:\가 아니라 C:¥였음
  • 이 확장들은 Ada 기능임. Ada에는 Call (Param_A => 1, Param_B => "Foo"); 형태의 레이블, 임의 진법 숫자에서의 밑줄(X : Integer := 1_000;), 중첩 서브프로그램, 범위 기반 검사가 있음
    • 글에서도 말하듯이 Pascal은 Ada보다 전부터 이런 기능들을 갖고 있었고, 진입점이 있는 작업 타입은 사실상 생성기로 볼 수 있음
      C가 당시 여러 다른 언어와 비교해 믿을 수 없을 정도로 원시적이었다는 점을 자주 잊는 것 같음
  • 내용과 별개로 이 책의 타이포그래피가 흥미로움. 아름답고 끔찍한 느낌이 동시에 있음
    일본어 표기법이나 커닝 규칙을 충분히 알지는 못하지만, 한자와 라틴 문자가 모두 들어간 가변폭 글꼴을 가져다가 고정폭 칸에 억지로 맞춰 넣은 것처럼 보임
    어쨌든 코드 예제가 내가 가진 많은 책들처럼 8pt 글꼴이 아닌 건 좋음
  • 생성기를 보니 Rust의 내부/외부 반복 문제와 try_fold()가 떠오름 (https://scribe.rip/@veedrac/rust-is-slow-and-i-am-the-cure-3...)
  • 특히 생성기를 보면 시대를 한참 앞서간 것 같음. Fujitsu가 긴 표준화 절차를 신경 쓰지 않았기 때문에 그냥 구현할 수 있었던 것일 수도 있음
    하지만 바로 그 이유 때문에 이런 확장들이 상대적으로 알려지지 않았고, 수십 년 뒤 현대 C/C++에서 다시 발견되고 재발명되어야 했던 듯함
    • Fujitsu가 아니라 MetaWare였음. MetaWare는 컴파일러 경험이 꽤 있는 회사였고, 같은 시기에 꽤 잘 알려진 Pascal 컴파일러도 갖고 있었음. Pascal에는 이미 중첩 함수가 있었음
    • C는 2의 보수조차 표준에 넣으면 안 된다고 고집한 사람들에게 장악되지 않았다면 훨씬 더 나은 언어가 될 수 있었음
    • 코루틴과 생성기는 그때 이미 잘 이해된 개념이었음. Icon을 보면 됨. 그래서 정말로 주된 이유는 표준화 부담을 걱정하지 않아도 됐다는 점에 가까워 보임