1 điểm bởi GN⁺ 2024-09-27 | 1 bình luận | Chia sẻ qua WhatsApp
  • Năm 1989, High C Compiler cho FM TOWNS không chỉ hỗ trợ môi trường DOS mà còn tích hợp nhiều tính năng ngôn ngữ hướng tới người dùng, vốn hiếm thấy ở các trình biên dịch C thời đó
  • Khi kết hợp với DOS extender của Phar Lap, nó trở thành trình biên dịch C 1st-party cho FM TOWNS trong dòng phát triển tận dụng 80386 32-bit trên môi trường MS-DOS 16-bit
  • Dấu gạch dưới trong literal số, đối số có nhãn, phạm vi case, hàm lồng nhau, generator... là những tính năng chỉ xuất hiện muộn hơn rất nhiều trong chuẩn C/C++, hoặc đến nay vẫn chưa có trong chuẩn
  • Hàm lồng nhau cung cấp “full function value” dưới dạng closure không thoát ra ngoài, truyền cả con trỏ hàm lẫn con trỏ ngữ cảnh, nên biểu đạt tốt hơn con trỏ hàm C thông thường
  • Generator được triển khai như lớp cú pháp đường trên hàm lồng nhau, hoạt động theo cấu trúc đơn giản: biến phần thân vòng lặp for của bên gọi thành một hàm lồng nhau rồi truyền nó làm đối số cho yield

Vị trí của FM TOWNS và High C

  • Một cuốn manual trình biên dịch C từ thập niên 1980, tìm thấy trong đống sách về FM TOWNS, hóa ra chứa lượng phần mở rộng ngôn ngữ phong phú hơn dự đoán
  • Muốn dùng C và các ngôn ngữ cùng họ trong môi trường thực tế thì trong thời gian dài vẫn cần đến phần mở rộng của nhà cung cấp
    • Ngày nay, trong hệ sinh thái xoay quanh GCC, Clang và MSVC, các phần mở rộng có xu hướng tập trung vào xử lý theo nền tảng hoặc kiểm soát chi tiết mức thấp
    • Vào thập niên 1980, có nhiều công ty nhỏ hơn cùng cạnh tranh để được chấp nhận, nên các tính năng mở rộng cũng đa dạng hơn
  • Phar Lap đã tạo ra một trong những DOS extender đầu tiên giúp tận dụng bộ xử lý 80386 32-bit trong môi trường MS-DOS 16-bit
  • MetaWare đã port High C Compiler sang SDK DOS extender của Phar Lap theo yêu cầu từ Phar Lap
  • Fujitsu tích hợp DOS extender của Phar Lap vào OS của nền tảng FM TOWNS dựa trên 803386, và High C trở thành trình biên dịch C 1st-party của nền tảng này
  • FM TOWNS ra mắt năm 1989, ngay trước khi chuẩn ANSI C đầu tiên là C89 được phê chuẩn

Những tiện ích nhỏ đi trước chuẩn

  • Dấu phân tách bằng gạch dưới trong literal số

    • Có thể chèn dấu gạch dưới phân tách vào literal số để các số dài dễ đọc hơn
    • C++ giới thiệu dấu phân tách bằng nháy đơn như 1'000'000 trong C++14
    • C mãi đến C23 mới đưa vào một tính năng tương tự
  • Đối số có nhãn

    • Có thể gắn tên cho đối số trong các hàm có nhiều tham số hoặc hay dùng những kiểu như bool, nơi ý nghĩa khó thể hiện rõ tại chỗ gọi
    • Đối số có nhãn của High C hoạt động tương tự một tính năng nổi tiếng của Python
      • Nhãn đối số là tùy chọn
      • Nếu có nhãn, có thể chỉ định đối số theo thứ tự bất kỳ bằng cú pháp argumentName => value
      • Có thể trộn đối số không nhãn với đối số có nhãn, nhưng mọi tham số của hàm đều phải có đối số tương ứng
    • Chuẩn C và C++ hiện vẫn chưa có tính năng này
  • Phạm vi case

    • Cung cấp khả năng khớp cả một khoảng giá trị cùng lúc, giống case low..high của Pascal
    • Chuẩn C và C++ chưa chấp nhận tính năng này

Hàm lồng nhau và full function value

  • High C cho phép khai báo hàm lồng nhau bên trong hàm khác, giống Pascal
  • Cách triển khai của nó gần với một dạng hoàn chỉnh hơn so với Pascal chuẩn hay phần mở rộng hàm lồng nhau của GCC
  • High C không chỉ cho phép khai báo hàm lồng nhau mà còn cho phép khai báo kiểu full function value
    • Khác với con trỏ hàm C truyền thống, nó chứa cả con trỏ hàm lẫn con trỏ ngữ cảnh
    • Nhờ đó có thể tìm lại ngữ cảnh mà hàm lồng nhau đã capture
    • Đây là closure không thoát ra ngoài, nên vòng đời không kéo dài đến sau khi hàm bao ngoài trả về
  • Phần mở rộng hàm lồng nhau của GCC cố tham chiếu hàm lồng nhau như con trỏ hàm thông thường bằng cách ghi mã thực thi được lên stack gọi để thunk con trỏ ngữ cảnh
    • Cách làm này dẫn đến rủi ro bảo mật lớn, khiến nhiều nền tảng phải vô hiệu hóa hoàn toàn tính năng đó
  • Tham chiếu tới hàm cục bộ trong High C có thể được dùng như giá trị hạng nhất, nhưng vòng đời không được kéo dài đến sau khi hàm bao ngoài trả về
  • Hàm lồng nhau còn có thể goto tới hàm cha
    • Tương tự block của Smalltalk, nó hỗ trợ thoát phi cục bộ, tức thoát ra ngoài hàm lồng nhau
    • Nhờ đó có thể tạo ra các hàm hoạt động như điều khiển luồng
  • Objective-C đến năm 2009 mới có blocks dùng được như escaping closure, còn C++ giới thiệu lambda vào năm 2011
  • Cả hai đều không có khả năng thoát phi cục bộ
  • Chuẩn C đến nay vẫn chưa có tính năng hàm lồng nhau chính thức

Coroutine generator

  • MetaWare nhấn mạnh mạnh tính năng generator đến mức dành hẳn một chương để nói về nó
  • High C đã hỗ trợ coroutine generator kiểu Python trong plain C ngay từ năm 1989
  • Hàm generator được khai báo bằng cú pháp void foo(Arg arguments) -> (Yield yields)
    • Bên trong hàm có thể gọi nhiều lần hàm ma thuật yield(values...) để tạo ra một chuỗi giá trị
    • Bên gọi sẽ duyệt lần lượt các giá trị được tạo ra bằng cú pháp vòng lặp for mới dạng for variable... <- foo(arguments...) do { ... }
  • Cách triển khai này có thể kết hợp phức tạp với hàm lồng nhau
    • Hàm lồng nhau bên trong generator có thể capture hành vi yield của generator bao ngoài
    • Hàm lồng nhau có thể tự gọi đệ quy để duyệt cây hoặc cấu trúc dữ liệu đệ quy, đồng thời yield ở mỗi bước
  • Kiểu này có vẻ khó triển khai trong Python hoặc nhiều ngôn ngữ coroutine generator phổ biến khác

Cách triển khai generator và khác biệt với ngôn ngữ chuẩn

  • Generator của High C hoạt động như lớp cú pháp đường trên hàm lồng nhau, không cần runtime cao cấp
  • Khai báo generator dạng void foo(Arg arguments) -> (Yield yields) tương đương với khai báo hàm thông thường void foo(void yield(Yield yields)!, Arg arguments)
    • yield là một tham số ngầm có kiểu “full function value”
    • Lời gọi yield(values) trong phần thân generator chỉ là lời gọi hàm bình thường tới tham số hàm ngầm này
  • Phần thân vòng lặp for phía bên gọi sẽ được chuyển thành một hàm lồng nhau
    • Hàm lồng nhau này được truyền làm đối số yield cho generator
    • Cấu trúc đơn giản nhưng hiệu quả
  • Vì hàm lồng nhau hỗ trợ thoát phi cục bộ, nên các lệnh break, continue, goto nhảy ra khỏi phần thân vòng lặp for cũng hoạt động bằng cách goto tới vị trí thích hợp bên ngoài vòng lặp
  • Khả năng chuẩn C tích hợp những tính năng như vậy là khá thấp
  • C++20 cung cấp tính năng coroutine rất linh hoạt nhưng cũng phức tạp, dựa trên biến đổi coroutine ở thời điểm biên dịch
    • Có lẽ có thể dùng nó để triển khai generator
    • Tuy nhiên, kết quả tạo ra có vẻ sẽ không kết hợp trực quan với hàm cục bộ theo cách này

1 bình luận

 
GN⁺ 2024-09-27
Các bình luận trên Hacker News
  • Năm 2011, tôi từng tổng hợp về vòng for dựa trên iterator. Đây đã là một trong những tính năng bị lãng quên từ lâu, và khi đó tôi cũng bàn tới việc nếu nó được đưa vào chuẩn C++ thì có thể sẽ trông như thế nào
    May mắn là tôi có một bản tiếng Anh của High C/C++ Language Reference
    http://jdebp.uk./FGA/metaware-iterator-driven-for.html
    http://jdebp.uk./Proposals/metaware-iterator-driven-for.html
    • Tôi tò mò không biết break hay return được biên dịch như thế nào. Có lẽ nó phải được biến đổi sao cho hàm yield trả về mã trạng thái rồi kiểm tra tại điểm gọi?
    • Có phải cái mặt cười lộn ngược đó là cố ý không?
  • Trong D, kể cả Das BetterC, có những tính năng như thế này: dấu gạch dưới trong literal số, phạm vi case, đối số đặt tên, hàm lồng nhau, hàm lồng nhau tĩnh, và tính năng tương tự generator
    Ví dụ có thể viết các dạng như int a = 1_234_567;, case 5 .. case 6:, test(b:3, a:4);
    Hàm lồng nhau tĩnh không thể truy cập biến frame của hàm bên ngoài, nên sẽ báo lỗi kiểu Error: static function test.foo.plus cannot access variable i in frame of function test.foo
    Tính năng tương tự generator nằm ở https://dlang.org/spec/statement.html#foreach_over_struct_an...
    • Trong suốt lúc đọc bài này, tôi cứ nghĩ đến D. Tôi có cảm giác Walter Bright sẽ xuất hiện trong phần bình luận
    • Tôi cũng cho rằng garbage collector của D là một tính năng thật sự tốt. Trong mã cấp thấp đôi khi cần quản lý bộ nhớ thủ công, nhưng thực tế có nhiều phần không mấy liên quan, và garbage collector giúp công việc dễ hơn rất nhiều
      Ví dụ nếu xây dựng một dịch vụ cache in-memory, tốt nhất là các mục cache tự thân không nên để garbage collector theo dõi. Vì garbage collector thường không biết mẫu truy cập thực tế nên có thể gây cản trở. Nhưng với hầu hết các thành phần khác của dịch vụ đó, có garbage collector lại phù hợp hơn
    • Tôi có một câu hỏi. Có ai biết vì sao mọi người ghét khái niệm hàm lồng nhau trong C không?
      Vì sao đối số đặt tên lại có dạng test(a:4, b:3) chứ không phải test(.a=4, b.=3);?
      Tôi cũng tò mò không biết trong C có thể xử lý kiểu hạng nhất như thế nào
  • Liên quan đến chuyện này, trình biên dịch C lcc-win đã thêm operator overloading, đối số hàm mặc định và function overloading. Trong tài liệu, hãy xem mục “generic functions” [1]
    Trình biên dịch C của Plan 9 cũng đưa vào nhiều phần mở rộng ngôn ngữ, trong đó một số như struct/union ẩn danh sau này đã đi vào chuẩn C. Hiện GCC chấp nhận cờ -fplan9-extensions [2], có thể bật một số tính năng khá hữu ích như tự động chuyển đổi con trỏ struct thành trường ẩn danh trong lời gọi hàm và phép gán
    [1] https://lcc-win32.services.net/C-Tutorial.pdf
    [2] https://gcc.gnu.org/onlinedocs/gcc/Unnamed-Fields.html
  • Không biết thiên tài nào đã tạo ra các tính năng này? Có vẻ trong công ty đó có người cực kỳ có tầm nhìn xa
    Thật tiếc là chúng không lan rộng ra thế giới để ảnh hưởng đến chuẩn ngôn ngữ. Thật đáng kinh ngạc khi những tính năng như vậy đã tồn tại từ lâu đến thế
    Hacker News trước đây cũng từng bàn về chuyện này: https://news.ycombinator.com/item?id=38938402
    Có bản PDF nào ở đâu đó không?
    • CLU từ giữa đến cuối thập niên 1970 đã có iterator, tức vòng lặp for với generator và yield [0]. Ngôn ngữ Icon cùng thời kỳ cũng có tính năng generator tương tự [1], dùng suspend thay cho yield. Theo tôi biết Ada (1983) cũng có tính năng như vậy
      Những tính năng ngôn ngữ này không hẳn là hoàn toàn vô danh
      [0] https://publications.csail.mit.edu/lcs/pubs/pdf/MIT-LCS-TR-2...
      [1] https://dl.acm.org/doi/pdf/10.1145/800055.802034
    • Trên Bitsavers có bản sao sổ tay tham chiếu HC 1.2 (1985)
      Nó mô tả cả dấu gạch dưới trong số, phạm vi case, tham số đặt tên, hàm lồng nhau và cả biến hàm hoàn chỉnh
      https://bitsavers.org/pdf/metaware/…
      Xem Appendix A, cách cuối tệp khoảng hơn 50 trang
    • MetaWare là một công ty biên dịch rất năng suất ở Santa Cruz trong thập niên 80–90. Tôi thích những gì họ làm, và văn hóa của họ cũng khá thú vị
      Hồi trước, khi còn học và viết code, tôi biết đến họ qua vài trang web hơi đáng ngờ
    • Điều này không quá đáng ngạc nhiên. Nếu đào sâu vào kho lưu trữ các ngôn ngữ lập trình bậc cao sau FORTRAN, Lisp, ALGOL, COBOL, bạn sẽ thấy rất nhiều ý tưởng ngôn ngữ như vậy
      Bạn cũng sẽ phát hiện lịch sử phong phú của các ngôn ngữ lập trình hệ thống. Và sẽ thấy thiết kế của C và Go giống nhau đến mức nào ở chỗ đã bỏ qua những gì đang diễn ra trong các hệ sinh thái khác cũng như các kinh nghiệm trong quá khứ
    • Thật tiếc khi những tính năng này trông như tính năng mới, thay vì là một phần trong danh sách tính năng tiêu chuẩn mà hầu hết ngôn ngữ lập trình cung cấp

Liên kết tới manual của compiler nằm ở https://winworldpc.com/product/metaware-high-c-cpp/33x
PDF manual C có ghi bản quyền năm 2007

  • Bài đăng trước đó và phần bình luận ở đây: https://news.ycombinator.com/item?id=38938402
    • Hôm nay Joe Groff đã đào lại nó trên FediVerse, nên có vẻ nó cũng được đăng lại ở đây
      https://f.duriansoftware.com/@joe/113195961485703110
    • Có vẻ tác giả đã đăng lại cùng nội dung lên một URL khác vào hôm qua. Kỳ lạ
  • Nếu bạn thắc mắc vì sao literal chuỗi trong ví dụ ở hình kết thúc bằng ¥n chứ không phải \n, có vẻ các ví dụ code này được viết bằng Shift-JIS. Trong Shift-JIS, vị trí của \ trong ASCII được thay bằng ¥
    • Ban đầu đó là JIS Roman [0], một biến thể ASCII tiếng Nhật từ năm 1969. Shift-JIS mãi về sau mới bổ sung hỗ trợ bộ ký tự byte kép
      [0] https://en.wikipedia.org/wiki/JIS_X_0201
    • Vấn đề là trong Shift-JIS, mã ASCII của dấu gạch chéo ngược cũng được dùng làm byte thứ hai của ký tự 2 byte. Vì vậy literal chuỗi tiếng Nhật trong C đôi khi không hoạt động đúng
      Với mục đích này thì EUC-JP tốt hơn, vì không có vấn đề đó. Trong Pascal, nếu dùng chú thích (* *) và không dùng chú thích { } thì dùng Shift-JIS cũng không gặp vấn đề này
    • Tác giả không cho biết cuốn sách này ra đời khi nào, và tìm cũng không thấy thông tin. Nhưng có lẽ khi sách được xuất bản thì chuẩn Shift-JIS vẫn chưa tồn tại
      Thay vào đó, nhiều khả năng JIS X 0201(https://en.m.wikipedia.org.org/wiki/JIS_X_0201), nền tảng của Shift-JIS, đã được sử dụng
    • Tương tự, prompt DOS tiếng Nhật là C:¥ chứ không phải C:\
  • Các phần mở rộng này là tính năng của Ada. Ada có nhãn dạng Call (Param_A => 1, Param_B => "Foo");, dấu gạch dưới trong số ở cơ số tùy ý (X : Integer := 1_000;), subprogram lồng nhau, và kiểm tra dựa trên phạm vi
    • Như bài viết cũng nói, Pascal đã có những tính năng này từ trước Ada, và kiểu task có điểm vào thực chất có thể xem là generator
      Có vẻ người ta thường quên rằng C thời đó thô sơ đến khó tin khi so với nhiều ngôn ngữ khác
  • Ngoài nội dung, typography của cuốn sách này khá thú vị. Nó vừa đẹp vừa kinh khủng
    Tôi không hiểu đủ về quy ước viết tiếng Nhật hay quy tắc kerning, nhưng trông như họ lấy một phông chữ độ rộng biến thiên có cả kanji và chữ Latin rồi cố nhét vào các ô cố định độ rộng
    Dù sao thì cũng tốt khi các ví dụ code không dùng cỡ chữ 8pt như nhiều cuốn sách tôi có
  • Nhìn generator làm tôi nhớ đến vấn đề lặp nội bộ/bên ngoài trong Rust và try_fold() (https://scribe.rip/@veedrac/rust-is-slow-and-i-am-the-cure-3...)
  • Đặc biệt khi nhìn vào generator, có cảm giác nó đi trước thời đại rất xa. Có thể Fujitsu đã có thể cứ thế triển khai vì không cần bận tâm đến quy trình chuẩn hóa dài dòng
    Nhưng có vẻ chính vì lý do đó mà các phần mở rộng này tương đối ít được biết đến, và nhiều thập kỷ sau mới phải được phát hiện lại và tái phát minh trong C/C++ hiện đại
    • Không phải Fujitsu mà là MetaWare. MetaWare là công ty có khá nhiều kinh nghiệm về compiler, và cùng thời đó họ cũng có một compiler Pascal khá nổi tiếng. Pascal vốn đã có hàm lồng nhau
    • C đã có thể trở thành một ngôn ngữ tốt hơn nhiều nếu không bị chi phối bởi những người khăng khăng rằng ngay cả số bù 2 cũng không nên được đưa vào chuẩn
    • Coroutine và generator khi đó đã là các khái niệm được hiểu rõ. Cứ nhìn Icon là thấy. Vì vậy lý do chính thực sự có vẻ gần với việc họ không phải lo về gánh nặng chuẩn hóa