1 คะแนน โดย GN⁺ 5 일 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • แปลงโค้ด Ruby ให้เป็น ไบนารีเนทีฟแบบสแตนด์อโลนที่รันได้เอง และมุ่งให้รันได้เร็วกว่า CRuby miniruby รุ่นล่าสุดโดยเฉลี่ยเรขาคณิตราว 11.6 เท่า ผ่านการทำ type inference ระดับทั้งโปรแกรมและการสร้างโค้ด C
  • ไปป์ไลน์การคอมไพล์เริ่มจากพาร์เซอร์ที่อิง Prism แปลง Ruby เป็น ข้อความ AST จากนั้นแบ็กเอนด์แบบ self-hosting จะทำ type inference และสร้างโค้ด C ก่อนคอมไพล์เป็นไบนารีสแตนด์อโลนด้วยคอมไพเลอร์ C มาตรฐาน
  • แบ็กเอนด์ของคอมไพเลอร์มีโครงสร้าง self-hosting ที่เขียนด้วย Ruby และหลังผ่านกระบวนการ bootstrap จะได้ gen2.c == gen3.c ทำให้ลูปการคอมไพล์ตัวเองซ้ำปิดสมบูรณ์
  • ใส่ การปรับแต่งตอนคอมไพล์ เช่น string concatenation flattening, value-type promotion, loop-invariant length hoisting, static symbol interning และ bigint auto-promotion พร้อมมีเอนจิน regexp ในตัว, bigint และรันไทม์แบบ single-header เพื่อลดการพึ่งพารันไทม์ภายนอก
  • แม้จะยังไม่รองรับ eval, metaprogramming, Thread และการจัดการ encoding ทั่วไป แต่ก็แสดงให้เห็นถึงความเป็นไปได้เชิงปฏิบัติของการคอมไพล์ Ruby แบบ AOT จากรูปแบบการแจกจ่ายที่ รันได้โดยไม่ต้องมี Ruby และช่องว่างด้านประสิทธิภาพที่ชัดเจนในงานคำนวณเข้มข้น

วิธีการทำงาน

  • ไปป์ไลน์การคอมไพล์ประกอบด้วยการพาร์สไฟล์ Ruby แล้วซีเรียลไลซ์เป็น ไฟล์ข้อความ AST จากนั้นทำ type inference และสร้างโค้ด C ก่อนสร้างเป็นไบนารีเนทีฟด้วยคอมไพเลอร์ C มาตรฐาน
  • spinel_parse ใช้ Prism และ libprism ในการพาร์ส Ruby และถ้ายังไม่มีไบนารี C จะใช้เส้นทางสำรองผ่าน CRuby และ Prism gem
  • spinel_codegen ทำงานเป็นไบนารีเนทีฟแบบ self-hosted โดยรับ AST แล้วทำ type inference + สร้างโค้ด C
  • ขั้นสุดท้ายคือคอมไพล์ซอร์ส C พร้อมรันไทม์เฮดเดอร์ด้วย cc -O2 -Ilib -lm และได้ผลลัพธ์เป็นไบนารีแบบ standalone

Self-Hosting

  • สาย bootstrap ทำงานโดยใช้ CRuby + spinel_parse.rb เพื่อสร้าง AST แล้วใช้ CRuby + spinel_codegen.rb เพื่อสร้าง gen1.c และ bin1 จากนั้นใช้ไบนารีที่สร้างขึ้นมาสร้าง gen2.c และ gen3.c ต่อจนลูปปิดสมบูรณ์
  • มีการระบุว่า gen2.c == gen3.c ซึ่งยืนยันว่าลูป bootstrap loop ปิดแล้ว
  • แบ็กเอนด์ spinel_codegen.rb ถูกเขียนด้วยส่วนย่อยของ Ruby ที่ Spinel คอมไพล์ได้เอง
    • classes, def, attr_accessor
    • if/case/while
    • each/map/select, yield
    • begin/rescue
    • การทำงานกับ String, Array, Hash และ File I/O
  • แบ็กเอนด์ไม่มี metaprogramming, eval, require

ประสิทธิภาพและเบนช์มาร์ก

  • การทดสอบผ่าน 74 รายการ และเบนช์มาร์กผ่าน 55 รายการ
  • ค่าเฉลี่ยเรขาคณิตจากเบนช์มาร์ก 28 รายการเร็วกว่า CRuby miniruby รุ่นล่าสุดประมาณ 11.6 เท่า
  • เกณฑ์เปรียบเทียบคือบิลด์ CRuby miniruby รุ่นล่าสุดที่ไม่มี bundled gem และแม้เทียบกับฐานที่เร็วกว่าอย่าง ruby 3.2.3 ของระบบ ก็ยังเหนือกว่าอย่างมากในเวิร์กโหลดคำนวณเข้มข้น
  • ประสิทธิภาพด้านการคำนวณ

    • life เร็วกว่า 86.7 เท่า: 20ms เทียบกับ 1,733ms
    • ackermann เร็วกว่า 74.8 เท่า: 5ms เทียบกับ 374ms
    • mandelbrot เร็วกว่า 58.1 เท่า: 25ms เทียบกับ 1,453ms
    • เวอร์ชันเรียกซ้ำของ fib เร็วกว่า 34.2 เท่า: 17ms เทียบกับ 581ms
    • nqueens เร็วกว่า 30.4 เท่า: 10ms เทียบกับ 304ms
    • tarai เร็วกว่า 28.8 เท่า: 16ms เทียบกับ 461ms
    • tak เร็วกว่า 24.2 เท่า: 22ms เทียบกับ 532ms
    • matmul เร็วกว่า 24.1 เท่า: 13ms เทียบกับ 313ms
    • sudoku เร็วกว่า 17.0 เท่า: 6ms เทียบกับ 102ms
    • partial_sums เร็วกว่า 16.1 เท่า: 93ms เทียบกับ 1,498ms
    • fannkuch เร็วกว่า 9.5 เท่า: 2ms เทียบกับ 19ms
    • sieve เร็วกว่า 8.5 เท่า: 39ms เทียบกับ 332ms
    • fasta เร็วกว่า 7.0 เท่า: 3ms เทียบกับ 21ms
  • โครงสร้างข้อมูลและ GC

    • rbtree เร็วกว่า 22.6 เท่า: 24ms เทียบกับ 543ms
    • splay tree เร็วกว่า 13.9 เท่า: 14ms เทียบกับ 195ms
    • huffman เร็วกว่า 9.8 เท่า: 6ms เทียบกับ 59ms
    • so_lists เร็วกว่า 5.4 เท่า: 76ms เทียบกับ 410ms
    • binary_trees เร็วกว่า 3.6 เท่า: 11ms เทียบกับ 40ms
    • linked_list เร็วกว่า 2.9 เท่า: 136ms เทียบกับ 388ms
    • gcbench เร็วกว่า 2.0 เท่า: 1,845ms เทียบกับ 3,641ms
  • โปรแกรมจริง

    • json_parse เร็วกว่า 10.1 เท่า: 39ms เทียบกับ 394ms
    • การคำนวณ bigint_fib 1000 หลัก เร็วกว่า 8.0 เท่า: 2ms เทียบกับ 16ms
    • ao_render เร็วกว่า 8.0 เท่า: 417ms เทียบกับ 3,334ms
    • pidigits เร็วกว่า 6.5 เท่า: 2ms เทียบกับ 13ms
    • str_concat เร็วกว่า 6.5 เท่า: 2ms เทียบกับ 13ms
    • template engine เร็วกว่า 6.2 เท่า: 152ms เทียบกับ 936ms
    • csv_process เร็วกว่า 3.7 เท่า: 234ms เทียบกับ 860ms
    • io_wordcount เร็วกว่า 2.9 เท่า: 33ms เทียบกับ 97ms

ความสามารถของ Ruby ที่รองรับ

  • ฟีเจอร์ Core รองรับ classes, inheritance, super, mixin แบบ include, attr_accessor, Struct.new, alias, module constants และ open classes สำหรับ built-in types
  • Control Flow รองรับ if/elsif/else, unless, case/when, pattern matching แบบ case/in, while, until, loop, for..in, break, next, return, catch/throw, &.
  • Blocks รองรับ yield, block_given?, &block, proc {}, Proc.new, -> x { }, method(:name) รวมถึงเมธอดที่ใช้บล็อกอย่าง each, map, select, reduce, sort_by, times, upto, downto
  • Exceptions รองรับ begin/rescue/ensure/retry, raise และคลาส exception ที่ผู้ใช้กำหนดเอง
  • Types ครอบคลุม Integer, Float, String, Array, Hash, Range, Time, StringIO, File, Regexp, Bigint, Fiber
    • ค่าที่มีหลายรูปแบบชนิดจัดการด้วย tagged unions
    • มี nullable object types T? สำหรับโครงสร้างข้อมูลแบบอ้างอิงตัวเอง
  • Global Variables อย่าง $name จะถูกคอมไพล์เป็นตัวแปร C แบบ static และตรวจพบ type mismatch ได้ตั้งแต่ตอนคอมไพล์
  • I/O รองรับ puts, print, printf, p, gets, ARGV, ENV[], File.read/write/open, system(), แบ็กติก

สตริง, เรกซ์เพรสชัน, ซิมโบล, Bigint, Fiber

  • Strings รองรับทั้งสตริง immutable และ mutable โดย << จะถูกยกระดับเป็นสตริง mutable sp_String อัตโนมัติเพื่อทำ append แบบ in-place ระดับ O(n)
  • +, interpolation, tr, ljust/rjust/center และเมธอดมาตรฐานทำงานได้กับทั้งสองรูปแบบของสตริง
  • การเปรียบเทียบอย่าง s[i] == "c" ถูกปรับแต่งให้เข้าถึงอาร์เรย์ของ char โดยตรง จึงทำงานแบบ ไม่ต้องจัดสรรหน่วยความจำเพิ่ม
  • การต่อแบบ a + b + c + d จะถูก flatten เหลือการเรียก sp_str_concat4 หรือ sp_str_concat_arr เพียงครั้งเดียว ทำให้มีการจัดสรรน้อยลง N-1 ครั้ง
  • str.split(sep) ภายในลูปจะนำ sp_StrArray ตัวเดิมกลับมาใช้ซ้ำ และใน csv_process ทำให้ ตัดการจัดสรร 4 ล้านครั้ง
  • Regexp ใช้ NFA regexp engine ในตัวโดยไม่พึ่งพาภายนอก
    • รองรับ =~, $1-$9, match?, gsub, sub, scan, split
  • Bigint ใช้จำนวนเต็มความแม่นยำไม่จำกัดที่อิง mruby-bigint
    • รูปแบบอย่าง q = q * k ในลูปจะถูกยกระดับอัตโนมัติ
    • ลิงก์เป็น static library และจะถูกรวมเข้ามาเมื่อมีการใช้งานจริงเท่านั้น
  • Fiber ให้ความสามารถด้าน concurrent แบบ cooperative บน ucontext_t
    • รองรับ Fiber.new, Fiber#resume, Fiber.yield และการส่งผ่านค่า
    • ตัวแปรอิสระจะถูก capture เป็นเซลล์ที่ย้ายขึ้น heap
  • Symbols ถูกอิมพลีเมนต์เป็นชนิด sp_sym ที่แยกจากสตริง
    • รักษาความหมาย :a != "a"
    • symbol literal จะถูก intern ตอนคอมไพล์เป็นค่าคงที่ SPS_name
    • String#to_sym จะใช้ dynamic pool เมื่อจำเป็นเท่านั้น
    • hash ที่ใช้คีย์เป็น symbol ใช้ sp_SymIntHash เพื่อเก็บคีย์จำนวนเต็มโดยตรงแทนสตริง จึงไม่ต้องมีทั้ง strcmp และการจัดสรรสตริงแบบไดนามิก

การจัดการหน่วยความจำและ value type

  • การจัดการหน่วยความจำใช้ mark-and-sweep GC พร้อม size-segregated free lists, non-recursive marking และ sticky mark bits
  • คลาสขนาดเล็กและเรียบง่ายจะถูกยกระดับเป็น value types อัตโนมัติและวางบนสแตก
    • เงื่อนไขคือมีฟิลด์ scalar ไม่เกิน 8 ฟิลด์
    • ไม่มี inheritance
    • ไม่มีการเปลี่ยนแปลงผ่านพารามิเตอร์
  • การจัดสรรคลาสที่มี 5 ฟิลด์จำนวน 1 ล้านครั้ง ลดเวลาจาก 85ms เหลือ 2ms
  • โปรแกรมที่ใช้แต่ value types จะ ไม่ต้อง export รันไทม์ GC ออกมาเลย

การปรับแต่ง

  • ทำ การปรับแต่งตอนคอมไพล์ หลายแบบโดยอิงจาก type inference ระดับทั้งโปรแกรม
  • Value-type promotion ทำให้คลาส immutable ขนาดเล็กกลายเป็นออบเจ็กต์ C struct บนสแตก เพื่อตัดโอเวอร์เฮดของ GC
  • Constant propagation ทำให้ค่าคงที่ลิเทอรัลธรรมดาอย่าง N = 100 ถูก inline ตรงที่ใช้งานโดยไม่ต้อง lookup ผ่าน cst_N
  • Loop-invariant length hoisting ทำให้ while i < arr.length คำนวณความยาวเพียงครั้งเดียวก่อนเข้าลูป
    • แต่ถ้ามีการเปลี่ยนออบเจ็กต์ผู้รับในบอดี เช่น arr.push การ hoist นี้จะถูกปิดใช้งาน
  • Method inlining จะใส่ static inline ให้เมธอดสั้นที่ไม่เรียกซ้ำและมีไม่เกิน 3 statement เพื่อกระตุ้นให้ gcc inline
  • String concat chain flattening ลดสายการต่อให้เหลือการเรียกครั้งเดียว เพื่อตัดการสร้างสตริงกลาง
  • Bigint auto-promotion ยกระดับรูปแบบการบวกแบบอ้างอิงตัวเองหรือการคูณซ้ำไปเป็น bigint โดยอัตโนมัติ
  • Bigint to_s ใช้ mpz_get_str ของ mruby-bigint เพื่อทำงานแบบ divide-and-conquer ระดับ O(n log²n)
  • Static symbol interning เปลี่ยน "literal".to_sym ให้เป็นค่าคงที่ SPS_<name> ตั้งแต่ตอนคอมไพล์ และจะใส่รันไทม์พูลก็ต่อเมื่อใช้ dynamic interning
  • ใน sub_range สตริงที่มีความยาวถูก hoist แล้วจะใช้ sp_str_sub_range_len เพื่อข้ามการเรียก strlen ภายใน
  • line.split(",") ภายในลูปจะใช้ sp_StrArray เดิมซ้ำ
  • Dead-code elimination ใช้ -ffunction-sections -fdata-sections และ --gc-sections เพื่อลบฟังก์ชันรันไทม์ที่ไม่ได้ใช้จากไบนารีสุดท้าย
  • Iterative inference early exit จะหยุดลูป fixed-point ทันทีถ้าอาร์เรย์ลายเซ็นของ param, return และ ivar ทั้ง 3 ชุดไม่เปลี่ยนอีกต่อไป
    • โปรแกรมส่วนใหญ่คอนเวอร์จใน 1-2 รอบแทนที่จะต้องวนครบ 4 รอบ
    • เวลา bootstrap ลดลงประมาณ 14%
  • parse_id_list byte walk เปลี่ยนพาร์เซอร์รายการฟิลด์ AST ที่ถูกเรียกระหว่าง self-compile ราว 120,000 ครั้ง จาก s.split(",") ไปเป็นการเดินด้วยมือผ่าน s.bytes[i] ทำให้การจัดสรรต่อการเรียกลดจาก N+1 เหลือ 2 ครั้ง
  • โค้ด C ที่สร้างขึ้นยังคง คอมไพล์ได้โดยไม่มี warning ภายใต้ระดับ warning ปกติ และฮาร์เนสใช้ -Werror เพื่อให้รีเกรสชันปรากฏทันที

สถาปัตยกรรม

  • โครงสร้างรีโพซิทอรีแบ่งเป็นองค์ประกอบต่อไปนี้
    • spinel: สคริปต์แรปเปอร์คำสั่งเดียว บน POSIX shell
    • spinel_parse.c: ฟรอนต์เอนด์ C จาก libprism ไปเป็นข้อความ AST จำนวน 1,061 บรรทัด
    • spinel_codegen.rb: แบ็กเอนด์คอมไพเลอร์จาก AST ไปเป็นโค้ด C จำนวน 21,109 บรรทัด
    • lib/sp_runtime.h: เฮดเดอร์ไลบรารีรันไทม์จำนวน 581 บรรทัด
    • lib/sp_bigint.c: จำนวนเต็มความแม่นยำไม่จำกัด 5,394 บรรทัด
    • lib/regexp/: เอนจิน regexp ในตัว 1,759 บรรทัด
    • test/: การทดสอบฟังก์ชัน 74 รายการ
    • benchmark/: เบนช์มาร์ก 55 รายการ
    • Makefile: ระบบอัตโนมัติสำหรับการบิลด์
  • รันไทม์ lib/sp_runtime.h รวม GC, การอิมพลีเมนต์ array/hash/string และส่วนสนับสนุนรันไทม์อื่น ๆ ไว้ใน ไฟล์เฮดเดอร์เดียว
  • โค้ด C ที่สร้างขึ้นจะ include เฮดเดอร์นี้ และลิงเกอร์จะดึงเฉพาะส่วนที่จำเป็นจาก libspinel_rt.a
    • bigint
    • regexp engine
  • พาร์เซอร์มี 2 อิมพลีเมนเทชัน
    • spinel_parse.c ลิงก์กับ libprism โดยตรงและทำงานได้ โดยไม่ต้องมี CRuby
    • spinel_parse.rb เป็น CRuby fallback ที่ใช้ Prism gem
  • พาร์เซอร์ทั้งสองสร้างผลลัพธ์ AST เหมือนกัน และแรปเปอร์ spinel จะเลือกใช้ไบนารี C ก่อนถ้าเป็นไปได้
  • require_relative จะถูก resolve ตั้งแต่ตอนพาร์สและไฟล์ที่อ้างอิงจะถูก inline เข้ามา

ข้อจำกัด

  • No eval: ไม่รองรับ eval, instance_eval, class_eval
  • No metaprogramming: ไม่รองรับ send, method_missing, define_method แบบไดนามิก
  • No threads: ไม่รองรับ Thread, Mutex และรองรับเฉพาะ Fiber
  • No encoding: สมมติว่าใช้ UTF-8 และ ASCII
  • No general lambda calculus: ไม่รองรับ -> x { } ที่ซ้อนลึกและการเรียก []

การพึ่งพาและโมเดลการรัน

  • การพึ่งพาตอนบิลด์คือไลบรารี C libprism และ CRuby สำหรับ bootstrap เริ่มต้น
  • ไม่มีการพึ่งพาตอนรันไทม์ และไบนารีที่สร้างขึ้นต้องการเพียง libc + libm
  • regexp ใช้เอนจินในตัวจึงไม่ต้องมีไลบรารีภายนอก
  • Bigint ถูกรวมมาในตัว แต่จะลิงก์เข้ามาเมื่อมีการใช้งานจริงเท่านั้น
  • Prism คือพาร์เซอร์ Ruby ที่ spinel_parse ใช้งาน
    • make deps จะดาวน์โหลด prism gem tarball จาก rubygems.org และแตกซอร์ส C ลงใน vendor/prism
    • ถ้าติดตั้ง prism gem ไว้แล้ว ระบบจะตรวจพบโดยอัตโนมัติ
    • สามารถกำหนดพาธเองได้ด้วย PRISM_DIR=/path/to/prism
  • CRuby จำเป็นเฉพาะในช่วง bootstrap แรกเท่านั้น และหลัง make แล้วไปป์ไลน์ทั้งหมดจะรันได้ โดยไม่ต้องมี Ruby

ประวัติโปรเจกต์

  • Spinel เริ่มต้นจากการอิมพลีเมนต์ด้วย C มีขนาด 18K lines และยังคงอยู่ในสาขา c-version
  • หลังจากนั้นมีการเขียนใหม่ด้วย Ruby ในสาขา ruby-v1
  • master ปัจจุบันคือเวอร์ชันที่เขียนใหม่อีกครั้งด้วยส่วนย่อยของ Ruby ที่สามารถ self-hosting ได้

ไลเซนส์

  • ใช้ MIT License
  • เป็นไปตามไฟล์ LICENSE

1 ความคิดเห็น

 
GN⁺ 5 일 전
ความคิดเห็นจาก Hacker News
  • ถ้าเป็นสิ่งที่ Matz ทำเอง ก็น่าจะเข้าใจข้อจำกัดของ Ruby semantics ดีอยู่แล้ว เลยรู้สึกเชื่อถือได้
    วิทยานิพนธ์ปริญญาโทของผมก็เป็น AOT JS compiler เหมือนกัน มันใช้งานได้จริง แต่ข้อจำกัดของข้อมูลขาเข้ามีเยอะมาก สุดท้ายเลยพับไป
    ตอนนั้นนักพัฒนา JS ยังไม่ค่อยคุ้นกับการรักษาข้อจำกัดเหล่านั้นด้วยตัวเอง และอินพุตที่ไม่อาจรู้ล่วงหน้าได้โดยเนื้อแท้อย่าง JSON.parse ก็เป็นอุปสรรคสำคัญ
    ตอนนี้ด้วย TypeScript มันอาจเป็นเรื่องที่ทำได้จริงมากกว่าสมัยนั้นมาก
    แค่ดู lambda calculus ทั่วไปก็จะเห็นชัดว่าการอนุมานชนิดข้อมูลมีขีดจำกัด และในงานของ Matt Might หรือโปรเจ็กต์ Shed-skin Python ก็มีข้อจำกัดคล้ายกันโผล่มาเหมือนกัน
    ผมสงสัยว่า eval, send, method_missing, define_method พบได้บ่อยแค่ไหนในโค้ด Ruby จริง ๆ และสงสัยด้วยว่าปกติจัดการกับการพาร์สแบบไม่มีชนิดข้อมูล เช่น อินพุต JSON กันอย่างไร

    • งานออกแบบนี้ดู pragmatic มาก
      การพาร์ส Ruby ยากจนแทบจะยากกว่าการแปลเสียอีก เลยใช้ Prism และผลลัพธ์คือสร้าง C ออกมา
      ตัว Ruby semantics พื้นฐานจริง ๆ ไม่ได้ยากขนาดนั้นที่จะอิมพลีเมนต์
      ตรงกันข้าม ผมกำลังง่วนอยู่กับ self-hosting AOT compiler เก่าที่เขียนด้วย Ruby ล้วน และดันยืนกรานจะใช้ parser ของตัวเอง เลยเลือกเส้นทางที่ยากกว่ามากโดยตั้งใจ
      ผมเรียนรู้ตั้งแต่เนิ่น ๆ ว่า 80% แรกนั้นทำแบบคร่าว ๆ ก็ยังรันโค้ด Ruby ได้เยอะพอสมควร แต่ “80% ที่สอง” ที่ยากจริง ๆ กลับไปกองอยู่กับสิ่งที่ Matz ตัดออกจากโปรเจ็กต์นี้และ mruby เช่น encoding หรือฟีเจอร์จิปาถะรอบข้างสารพัด
      พูดตามตรง Ruby มีฟีเจอร์อยู่ไม่น้อยที่ผมไม่เคยเห็นในโค้ดจริงเลยสักครั้ง ดังนั้นถ้าบางอย่างจะ deprecated ก็คงไม่แปลก
      send, method_missing, define_method เจอบ่อยมาก
      ข้อจำกัดก็คล้าย mruby และถึงมีข้อจำกัดแบบนั้นก็ยังมีที่ให้ใช้งานอยู่
      การรองรับ send, method_missing, define_method ค่อนข้างทำได้ไม่ยาก
      แต่การรองรับ eval() นั้นทรมานมาก
      อย่างไรก็ตาม สัดส่วนใหญ่ของ eval() ใน Ruby สามารถลดรูปแบบคงที่ให้เป็น instance_eval แบบ block version ได้ และในกรณีนั้น AOT compile ก็จะง่ายขึ้นมาก
      ตัวอย่างเช่น ถ้ารู้สตริงที่ใส่เข้า eval() แบบคงที่ได้ หรือสามารถแยกวิเคราะห์มันได้ ก็มีทางแก้ได้เยอะ
      ในทางปฏิบัติ การใช้ eval() จำนวนมากนั้นไม่จำเป็นหรือแทบเป็นแค่การอ้อมข้อจำกัดของ introspection แบบง่าย ๆ ซึ่งจัดการได้ด้วยการตรวจสอบแบบสถิต
      คอมไพเลอร์ของผมเองก็คิดว่าจะไปลงมือที่จุดนั้นก่อนถ้ามันกลายเป็นคอขวด
    • ต้องมีฟีเจอร์แบบนี้อยู่เยอะพอสมควรถึงจะทำ magic แบบ Rails ได้
      การรับ JSON แบบไม่มีชนิดข้อมูลก็น่าจะใช้กลไกพวกนี้เหมือนกัน
      ถ้าตัดสิ่งเหล่านั้นออกไป ก็จะเหลือภาษาที่เล็กและอ่านง่าย ซึ่งแม้จะไม่ strong type เท่า Crystal แต่ก็ไม่ได้พึ่ง metaprogramming หนักเท่า Ruby ทางการ
      เพราะงั้นศักยภาพก็ดูสูงพอสมควร แต่สุดท้ายก็คงต้องรอเวลาเป็นตัวตัดสิน
    • ถ้าเป็นวิธีคอมไพล์ Ruby ไปเป็น Objective-C ก็เหมือนจะรองรับฟีเจอร์ Ruby ได้ครบทั้งหมด พร้อมทั้งเร็วกว่า Ruby แบบ interpreter ได้ด้วย
    • ผมเป็นฝั่งที่ใช้ eval บ่อย
      จะไม่ใช้ก็ได้แหละ แต่สำหรับผมแบบนั้นมัน ergonomic กว่า
    • จากประสบการณ์ของผม สิ่งที่น่าสนใจคือแพตเทิร์นการใช้ eval, exec, define_method และการสร้างคลาสใหม่ด้วย Class.new, Struct.new
      การใช้งานส่วนใหญ่ของพวกนี้มักกระจุกอยู่ตอน boot แอป หรือระหว่าง require ไฟล์ และในแง่หนึ่งมันก็คล้ายขั้นตอนคอมไพล์อยู่แล้ว
  • นี่คือสิ่งที่ Matz เพิ่งประกาศในงาน RubyKaigi 2026
    แม้จะยังเป็นงานทดลอง แต่ก็ทำขึ้นในเวลาประมาณหนึ่งเดือนด้วยความช่วยเหลือจาก Claude และเดโมสดก็สำเร็จด้วย
    ชื่อนี้มาจากแมวตัวใหม่ของ Matz และชื่อแมวนั้นก็มาจากชื่อแมวใน Card Captor Sakura ซึ่งต่อจากนั้นก็ไปจับคู่กับตัวละครที่ชื่อ Ruby อีกที

    • คนชอบพูดกันว่า AI จะสร้างโปรแกรมตั้งแต่ต้นจนจบได้หมด แต่ผมว่าภาพที่สมจริงกว่าคือมันจะเปลี่ยน 10x programmer ให้กลายเป็น 100x programmer
      สำหรับคนอย่าง Matz มันอาจเหมือนดันจาก 100x ไปเป็น 500x เลยก็ได้
    • ภาพจำล่าสุดของผมเกี่ยวกับ Spinel คือฝั่ง Steven Universe เลยไม่เก็ตมุก Spinel/Ruby (Moon) pun เลยสักนิด แต่พอรู้แล้วก็อารมณ์ดีไปทั้งวัน
    • ตอนแรกผมก็นึกว่าเป็นเรื่องของ แร่ spinel แน่นอนเลย :)
      https://en.wikipedia.org/wiki/Spinel
    • ขอบคุณ
      ดูเหมือนว่าวิดีโอยังไม่ขึ้นเป็นไลฟ์ และน่าจะกำลังทยอยอัปขึ้นช่องนี้ทีละคลิป
      https://www.youtube.com/@rubykaigi4884/videos
    • เรื่องที่มาของชื่อแมวตัวนั้นฟังดูน่าสงสัยพอสมควร ถ้านึกถึง ดราม่า Ruby Central กับความสัมพันธ์กับผู้ก่อตั้ง Spinel.coop
      แม้แต่ชื่อโปรเจ็กต์เองก็ให้ความรู้สึกเหมือนตั้งจากอารมณ์
  • เห็นได้ชัดว่าน่าประทับใจมาก แต่ก็ดูเหมือนจะบำรุงรักษาไม่ได้เลยถ้าไม่มี AI agent
    spinel_codegen.rb ยาว 21,000 บรรทัด และบางเมธอดซ้อนลึกถึง 15 ชั้น
    โค้ดคอมไพเลอร์เดิมทีก็ยากที่จะเขียนให้สวยอยู่แล้ว แต่ชิ้นนี้ดูยากมากเป็นพิเศษสำหรับให้คนดูแลต่อ

    • โค้ดคอมไพเลอร์จริง ๆ แล้วทำให้ สวย ได้ ถ้ามีเวลาเพียงพอ
      คอมไพเลอร์มีเส้นแบ่ง subsystem ชัดเจน และ handoff ตามแต่ละขั้นก็ค่อนข้างชัด จึงเป็นงานประเภทที่ modular ได้ง่ายที่สุดอย่างหนึ่งด้วยซ้ำ
      ปัญหามักอยู่ที่ทำให้มันรันได้ก่อนแล้วไม่มีเวลา refactor หลังจากนั้น พอเป็นแบบนั้นความเละก็จะพอกพูนต่อไปเรื่อย ๆ
    • spinel_codegen.rb นี่แทบระดับ eldritch horror เลย
      เวลาใช้ Claude ผมก็ได้ spaghetti code แบบนี้ออกมาตลอด เลยเคยสงสัยว่าตัวเองทำอะไรผิดหรือเปล่า
      แต่พอเห็นว่าแม้แต่โปรเจ็กต์ที่น่าสนใจจริง ๆ จากคนที่ผมมองว่าเป็นโปรแกรมเมอร์ระดับท็อปก็ยังมีคุณภาพโค้ดแย่เป็นหย่อม ๆ เหมือนกัน ก็รู้สึกว่าไม่ได้เป็นอยู่คนเดียว
      อย่าง infer_comparison_type() ยังไม่ใช่กรณีเลวร้ายสุด และก็ไม่ได้อ่านยากอะไร แต่จริง ๆ มีวิธีเขียนที่ง่ายและชัดเจนกว่านี้มาก ทว่า Claude กลับไปไม่ถึง
      ถ้ารวม comparison operator ไว้ใน Set แล้วใช้ include? จัดการ ก็จะสั้นกว่า เร็วกว่า อ่านง่ายกว่า และดูแลง่ายกว่า
      แต่ Claude มักจะไหลไปทาง สาย if-return ต่อเนื่อง เสมอ แถมให้ความรู้สึกว่าไม่ค่อยคุ้นแม้แต่กับ if-else ด้วย
      โค้ดเบสที่ Claude เขียนให้ผมก็เต็มไปด้วยแพตเทิร์นแบบนั้น และตอนนี้ก็รู้แล้วว่าไม่ได้เป็นกับผมคนเดียว
      ในทางกลับกัน ไฟล์อื่น ๆ ดีกว่ามาก โดยเฉพาะไดเรกทอรี lib ที่ดูเหมือนจะสอดคล้องกับไดเรกทอรี ext ของ Ruby repo หลัก และคุณภาพก็ถือว่าดี
      ตัว API ก็ได้รับอิทธิพลจาก MRI Ruby อย่างชัดเจน และแม้อิมพลีเมนเทชันจะแตกต่างอยู่มาก แต่เหมือน Matz จะชี้นำให้บางส่วนของ API ต้นฉบับยังคงหน้าตาคล้ายเดิม เลยทำให้ผลลัพธ์ดูเป็นระเบียบขึ้น
      [1] https://github.com/matz/spinel/blob/98d1179670e4d6486bbd1547...
    • ในช่วงนี้ผมว่าเรื่องที่คนจะดูแลต่อด้วยมือได้หรือไม่ยังไม่สำคัญขนาดนั้น
      ขอแค่ผ่านทั้งการทดสอบและ benchmark ผมก็พอใจแล้ว
      แต่ไฟล์ขนาดมหึมาแบบนี้ AI เองจะจัดการได้ง่ายหรือเปล่าก็ยังน่าสงสัย
      ผมพยายามจำกัดไฟล์ไม่ให้เกินประมาณ 300 บรรทัด และคิดว่าโค้ดที่คนเข้าใจง่ายก็น่าจะง่ายสำหรับ coding agents ด้วยเหมือนกัน
  • ข้อจำกัดมีดังนี้
    No eval: eval, instance_eval, class_eval
    No metaprogramming: send, method_missing, define_method (แบบไดนามิก)
    No threads: Thread, Mutex (รองรับ Fiber)
    No encoding: สมมติว่าเป็น UTF-8/ASCII
    No general lambda calculus: -> x { } แบบซ้อนลึกพร้อมการเรียก []
    สำหรับผม การสมมติว่าเป็น UTF-8/ASCII ไม่ใช่ข้อจำกัดใหญ่ แต่ที่เหลือน่าจะเป็นข้อจำกัดจริงสำหรับโปรแกรมจำนวนมากพอสมควร
    และถ้าจะใส่กลับเข้าไป ก็ดูเหมือนต้องใช้แรงงานอีกมาก

    • แบบนี้ magic ของ Ruby ก็หายไปเยอะเลย
  • ผมใช้ Ruby มานาน และจากมุมของคนที่เคยใช้ฟีเจอร์ทั้งหมดที่ไล่มานั้น จริง ๆ แล้วสิ่งที่ผมอยากได้หลังผ่านการคัดสรรตามธรรมชาติกลับเป็น Ruby แบบเรียบง่าย อย่างนี้
    มันง่ายกว่า เข้าใจง่ายกว่า แต่ยังคงสุนทรียะแบบ Ruby เอาไว้
    ตอนนี้ด้วย LLM ประสิทธิภาพการสร้างโค้ดสูงมาก จนไม่จำเป็นต้องลด boilerplate ด้วย metaprogramming เพื่อเพิ่ม productivity แบบเมื่อก่อนอีกแล้ว
    เพราะสัดส่วนที่นักพัฒนาต้องเขียนโค้ดเองจริง ๆ ก็น้อยลงอยู่แล้ว

    • ถ้าสิ่งที่ต้องการมีแค่ Ruby aesthetic จริง ๆ Crystal ก็อาจเหมาะมาก
      ไวยากรณ์คล้ายกันและมี static type system จึงต่อยอดไปสู่โค้ดที่คอมไพล์แล้วมีประสิทธิภาพมากกว่า
    • การไม่มี eval ผมยังมองว่าอาจดีกว่า แต่การไม่มี threads และ mutexes ด้วยนี่น่าเสียดาย
      การไม่มี define_method ก็พอเข้าใจได้เมื่อคิดถึงการใช้งานของมัน
      แต่ send กับ method_missing นั้นพบได้บ่อยในไลบรารีเดิม และก็ดูไม่น่าจะยากมากที่จะอิมพลีเมนต์ด้วยการสร้าง memory lookup table ตั้งแต่ตอนคอมไพล์
      เลยไม่แน่ใจว่านี่เป็นสิ่งที่จงใจตัดออก หรือแค่ยังไปไม่ถึงตรงนั้น
      ผมหวังว่าอย่างหลัง แต่ไม่ว่าอย่างน้อยตอนนี้ก็คงยังเอาไปใช้จริงในงานได้ยากเพราะปัญหา compatibility
    • ข้อดีของ metaprogramming เดิมทีไม่ใช่ การเขียนโค้ดให้น้อยลง
      แต่มันคือ การลดปริมาณโค้ดที่ต้องอ่าน
  • นี่เจ๋งมากจริง ๆ และผมรอ AOT compiler สำหรับ Ruby มานานแล้ว
    แค่เสียดายที่ไม่มี fallback สำหรับ eval หรือ metaprogramming แต่ก็ดูเหมือนตั้งใจโฟกัสที่ subset เล็ก ๆ ที่ประสิทธิภาพสูง
    ผมหวังว่า gem ที่สร้างด้วย AOT compiler นี้จะทำงานร่วมกับ MRI ได้ดี
    ฝั่งการแพ็กหรือบันเดิล Ruby มาตรฐานกับ gem นั้นยังคงต้องพึ่ง tebako, kompo, ocran และในอดีตก็เคยมีโปรเจ็กต์อย่าง ruby-packer, traveling ruby, jruby warbler
    การมีตัวเลือกเพิ่มมาอีกหนึ่งอย่างถือว่าดี แต่ผมก็ยังอยากเห็น เวอร์ชันตัดสินเกม ที่มี UX ฝั่งนักพัฒนาดีกว่านี้

    • ใช่เลย ช่วงหลัง ๆ ผมเองก็ต้อง fork warbler เหมือนกัน
      เพราะมันไม่ได้อัปเดตมานานเกินไปแล้ว
  • ผมสงสัยว่าทำไมถึง no threads
    Ruby scheduler กับ pthread implementation ข้างใต้ก็ดูเหมือนน่าจะทำงานได้ดีในฝั่ง C อยู่แล้ว หรือว่าตั้งใจจะทำแบบ zero dependency
    ถ้าไม่ได้มีแผนจะใส่เป็น optional extension ทีหลัง หรือไม่ได้แค่ยังไม่ทันทำ การเลือกแบบนี้ก็ดูแปลกอยู่เหมือนกัน

    • ผมยังไม่เห็นหลักฐานว่ามีการ ตัดสินใจว่าจะไม่รองรับ เรื่องนี้โดยเจตนา
      น่าจะเป็นแค่ว่ายังทำไปไม่ถึงตรงนั้นมากกว่า
      การทำ multithreading ให้ถูกต้องนั้นเดิมทีก็ยากมากอยู่แล้ว
  • น่าทึ่งที่ทำได้ในเวลาแค่เดือนเศษ
    จะพูดเรื่อง AI ยังไงก็ตาม ถ้าอยู่ในมือของ นักพัฒนาที่มีฝีมือ มันสร้างแรงเร่งความเร็วได้มหาศาลจริง ๆ

    • ทั้งอุตสาหกรรมกำลังเริ่มด้วยการเซ็ต agent harness, SOUL.md, permission settings, skills, MCPs, hooks, env กันเต็มไปหมด
      แต่ Matz ให้ความรู้สึกว่าแค่ gem env|info กับ find ก็พอแล้ว
  • ในเมื่อ Matz เป็นคนทำเอง ก็สงสัยว่ามีโอกาสจริงจังแค่ไหนที่สิ่งนี้จะกลายเป็นส่วนหนึ่งของ Ruby core ในอนาคต
    แล้วถ้าเป็นแบบนั้น มันจะเป็นภัยต่อ Crystal มากแค่ไหนด้วย

    • Crystal มี static type system แบบชัดเจน และตัวภาษาก็ถูกออกแบบมาเพื่อ AOT compile ตั้งแต่ระดับภาษา
      คุณลักษณะพวกนี้แทบจะจำเป็นสำหรับการคอมไพล์และดูแลโปรแกรมขนาดใหญ่
      ขณะที่สิ่งนี้รองรับแค่ subset ของ Ruby ที่มีข้อจำกัด ดังนั้น gem ยอดนิยมส่วนใหญ่ของ Ruby ก็น่าจะรันตรง ๆ ไม่ได้
      ในแง่ที่มันเป็น subset ของภาษาที่มุ่งสู่การคอมไพล์เป็น C มันดูใกล้กับ PreScheme มากกว่า
      ณ ตอนนี้ผมยังไม่มองว่าทั้งสองอย่างกำลังแข่งกันตรง ๆ ในพื้นที่เดียวกัน
      Ruby แบบสมบูรณ์นั้นแทบแน่นอนว่ายังต้องพึ่ง JIT
      [1]: https://prescheme.org/
    • ถ้ามองอีกมุม สุดท้ายแล้ว LLM ก็น่าจะไปถึงจุดที่สามารถพ่น formal specification ออกมาในภาษาอะไรก็ได้ที่เราต้องการ
      มันเหมือนเป็นการล้างแค้นของเครื่องมืออย่าง Rational Unified Process กับ Enterprise Architect
      ต่างกันแค่ว่าแทนที่จะได้ UML diagram เราจะได้ไฟล์ markdown มาแทน
  • สิ่งนี้น่าจะมีประโยชน์ในฝั่ง infrastructure tools
    เช่น อาจจินตนาการถึง bundler ที่เขียนด้วย Ruby แต่คอมไพล์แบบสถิตได้ จนทำหน้าที่เป็นเครื่องมือติดตั้ง Ruby แบบ RVM ไปด้วยในตัว
    buildpack ของ Ruby แบบเดิมก็เขียนด้วย Ruby แต่ต้อง bootstrap ด้วย bash ทำให้ชวนหงุดหงิดและเกิด edge case ตามมา
    CNB เขียนด้วย Rust เพื่อเลี่ยงปัญหานั้น และแนวคิดเรื่องการแจกจ่ายเป็น single binary แบบไร้ dependency นั้นทรงพลังมากจริง ๆ