1 คะแนน โดย GN⁺ 3 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • Jam เป็นภาษาที่ยังอยู่ก่อน v1.0 ซึ่งมุ่งให้ใช้งานได้ทันทีแบบภาษาในตระกูล C พร้อมทั้งความปลอดภัย เส้นโค้งการเรียนรู้ต่ำ และประสิทธิภาพสูง โดยไม่มี GC
  • แกนหลักคือ mutable value semantics และระบบ drop แบบ Rust โดยให้คอมไพเลอร์จัดการ ownership, borrow และการ cleanup อัตโนมัติ โดยไม่เปิดเผยไวยากรณ์ reference หรือ lifetime ให้โค้ดผู้ใช้
  • โมเดลการ initialize หลีกเลี่ยงทั้ง undefined และการ initialize เป็น zero แบบแฝง และจัดการ lazy initialization กับ out-parameter ด้วยการวิเคราะห์ Maybe(T) และ unsafeAssumeInit()
  • export เปิดเผยฟังก์ชัน Jam เป็น C ABI และออกแบบให้ Jam struct มี layout ที่เข้ากันได้กับ C เพื่อลดภาระการทำ unsafe shim หรือ annotation แบบ repr แยกต่างหาก
  • คอมไพเลอร์ยังอยู่ในขั้น bootstrap ที่ implemented ด้วย C++ และยังไม่เปิดเผยต่อสาธารณะ โดยมีแผนจะโอเพนซอร์สหลังจากสร้างโปรเจกต์ distinct 108 โปรเจกต์ด้วย Jam

ตำแหน่งของภาษาที่ Jam มุ่งหมาย

  • Jam ยังอยู่ก่อน v1.0 และกลไกที่อธิบายอยู่ตอนนี้ทำงานในคอมไพเลอร์แล้ว แต่รายละเอียดก่อน stabilize อาจเปลี่ยนแปลงได้
  • เป้าหมายคือสร้างภาษาที่ปลอดภัยซึ่งลด bug class ของ C ขณะยังรักษา ความรู้สึกแบบตระกูล C ที่เข้าใจได้ทันทีเหมือน Go, Zig และ modern C
  • แกนกลางของการออกแบบมี 2 อย่าง
    • Mutable value semantics ของ Racordon, Abrahams et al. 2022
    • drop system ของ Rust
  • จุดเริ่มต้นมาจากปัญหาที่ว่า ทีมจริงมีระดับทักษะปะปนกัน และสมาชิกที่ชำนาญน้อยกว่ามีโอกาสทำพลาดสูง จึงควรให้ภาษาช่วยกันข้อผิดพลาดได้มากขึ้นก่อนถึงขั้น code review

ความต่างจาก Rust, Zig และ C++

  • Rust มีปรัชญาด้านความปลอดภัยที่แข็งแรง แต่ช่องว่างระหว่าง “พอใช้ Rust ได้ระดับหนึ่ง” กับ “ทำงานได้ produktive ด้วย Rust” ค่อนข้างใหญ่ ทำให้เส้นโค้งการเรียนรู้เป็นภาระต่อทีมได้
  • Zig ให้พื้นผิวภาษาเล็กและ mental model ที่เข้าถึงทันที ใกล้เคียงภาษา C-like แต่ไม่ใช่ภาษาที่ปลอดภัยในระดับภาษา
    • การป้องกัน uninitialized read, manual cleanup และ use-after-free ไม่ได้ถูกบังคับในระดับภาษา
    • โปรเจกต์ production ขนาดใหญ่ใน Zig หรือ C++ พึ่งพาเครื่องมือตรวจสอบอย่าง Valgrind, AddressSanitizer และ fuzzing อย่างมาก
  • ในยุค AI มองว่า production code จำนวนมากจะถูกเขียนหรือร่างโดยเครื่องมือแทนมนุษย์ และคอขวดจะย้ายจาก code writing ไปเป็น code review
    • ปริมาณโค้ดเพิ่มขึ้น ขณะที่ review surface ค่อนข้างคงที่ ดังนั้นคอมไพเลอร์จึงต้องจับบั๊กให้ได้มากขึ้น

ระบบ drop อัตโนมัติ

  • binding ใน Jam เป็นเจ้าของค่า และเมื่อ binding ของ type ที่มี drop-bearing หลุดจาก scope คอมไพเลอร์จะสังเคราะห์การเรียก drop ให้
  • ตัวอย่าง type File ประกาศ fn drop(self: mut File) และใน useFile() เขียนเพียง const f: File = { fd: 7 };
    • ไม่มี explicit cleanup, defer หรือเครื่องหมายบอกจุดสิ้นสุด lifetime
    • ใน LLVM IR จะสร้าง call void @__drop_File(ptr %1) ไว้ก่อน ret
  • ชื่อแบบ mangled อย่าง __drop_File ทำให้ฟังก์ชัน drop ของหลาย type ไม่ชนกันในระดับ LLVM
  • self: mut File ถูก lowering เป็น pointer parameter และ call site ส่ง address ของ binding โดยตรง
  • ใน Zig ต้องระบุ defer f.deinit() เพื่อทำ cleanup แบบเดียวกัน
    • ถ้าลบบรรทัดนั้น การเรียก deinit ใน IR ก็หายไปด้วย
    • file descriptor leak เกิดขึ้นเมื่อ programmer ลืม cleanup
  • C++ RAII ก็เรียก destructor อัตโนมัติเมื่อออกจาก scope แต่ Jam เลือกใช้โมเดล drop ที่เรียบง่ายของ Rust
    • แนวทางคือหลีกเลี่ยงความซับซ้อนอย่าง rule of 0/3/5 ของ C++, virtual destructor, constructor exception, destructor exception, std::exit, std::abort, longjmp และ signal
    • Jam มี drop function หนึ่งตัวต่อ type และเรียกเมื่อออกจาก scope ทุกแบบ

การ initialize และ Maybe(T)

  • Jam ไม่มีค่า undefined และไม่สามารถประกาศ binding โดยไม่มีค่าได้
    • var และ const ทุกตัวต้องมี initializer จริง
    • struct จะคำนวณค่าของ field ก่อน แล้วสร้างด้วย struct literal จากนั้นจึง bind
  • Zig อนุญาต var f: File = undefined; return f.fd; และตอน runtime อาจอ่าน stack garbage ได้
    • ใน Debug mode จะเติม 0xaa เพื่อให้เห็นการ misuse
    • ใน Release mode จะเป็น arbitrary bytes
  • Go initialize var ทุกตัวเป็น zero เพื่อกัน garbage read แต่มีต้นทุนในการเขียน zero pattern แม้กับ field ที่กำลังจะถูก overwrite ทันที
  • Jam หลีกเลี่ยงทั้ง undefined และ implicit zero
  • สำหรับ lazy initialization และ out-parameter ใช้ Maybe(T)
    • empty() สร้าง slot ที่มี contents ยังไม่มีความหมาย
    • write() เติมค่าใน slot
    • unsafeAssumeInit() ดึงค่าออกมา
  • lint pass ติดตามว่า slot ถูก write แล้วหรือไม่ และปฏิเสธการเรียก unsafeAssumeInit() ที่ analyzer พิสูจน์ initialization ไม่ได้เป็น compile error
    • prefix unsafe ยังคงเป็น anchor ที่ human และ AI reviewer สามารถ grep ได้

Scope exit, return, break, continue

  • compiler ติดตาม drop scope stack และ push scope ใหม่ที่ lexical block boundary แต่ละครั้ง
  • เมื่อ block จบหรือกำลังออกด้วย branch จะ emit drop ของ binding ใน scope นั้นก่อน
    • binding ภายใน if, else, arm ของ match, while และ body ของ for จะถูก drop เมื่อจบ block นั้น
    • return ใน nested block จะ drop active scope จากชั้นในสุดออกมาก่อน ret จริง
    • break และ continue จะ drop scope ที่เปิดอยู่ใน loop body แล้วจึงไปยัง loop exit หรือ iteration ถัดไป
  • ในตัวอย่าง nested break นั้น outer ถูก drop เมื่อจบ iteration 0 และใน break path ของ iteration 1 จะ drop ตามลำดับ inner แล้ว outer

Parameter mode และการตัด first-class reference ออก

  • ในการเรียกฟังก์ชัน binding จะถูก drop หรือไม่ถูกกำหนดโดย parameter mode
  • mode เริ่มต้นคือ read-only borrow
    • callee อ่านค่า และ binding ของ caller ยังคงอยู่ในสถานะ initialized
    • ไม่มีการ drop เมื่อ call return
  • mut คือ exclusive read-write borrow
    • binding ของ caller ยังคงอยู่ในสถานะ initialized หลัง call
  • มีเพียง move เท่านั้นที่ consume ค่า
    • callee รับ ownership และ drop เมื่อจบ callee
    • binding ของ caller กลายเป็น Uninit หลัง call และถ้าอ่านจะเป็น compile error
  • ไม่มี marker ที่ call site และรูปแบบ f(x) เหมือนกันในทุก mode
  • Jam ไม่มี first-class reference type
    • ไม่สามารถเก็บ borrow ไว้ใน variable, return ออกไป หรือเก็บใน struct field ได้
    • parameter borrow มีอยู่เฉพาะระหว่าง call-frame และหมดอายุเมื่อ call return
    • เหตุผลที่ไม่ต้องมี lifetime annotation คือไม่มี lifetime ให้ attach
  • collection API ก็ยังรักษารูปทรงแบบ value-shaped
    • v[i] = x ถูก desugar เป็น v.setAt(i, x)
    • let y = v[i] เป็น getter v.at(i) ที่คืน element เป็น value
  • call site exclusivity check ตรวจสอบ path overlap ของ borrow set ที่ argument สร้างขึ้น
    • swap(p.x, p.y) เป็น sub-path ที่ disjoint จึง OK
    • moveX(p, p.x) เป็น error เพราะ p กับ p.x overlap กัน

C ABI และ FFI

  • native ABI ของ Rust ไม่ stable ดังนั้นเมื่อข้าม distribution boundary ต้อง encode ใหม่ในรูปแบบ C
    • การ dereference raw pointer เป็น unsafe
    • ownership ถูกส่งต่อเองด้วย Box::into_raw และ Box::from_raw
    • เมื่อส่ง struct by value ต้องมี annotation แยกอย่าง #[repr(C)]
    • เครื่องมืออย่าง cbindgen และ abi_stable มีขึ้นเพื่อลดงาน manual ตรง boundary นี้
  • Jam ไม่มี first-class reference, lifetime และ niche-packed layout จึงมองว่า Jam value เป็น value-shaped all the way down
    • Jam struct ถูกออกแบบให้มี C-compatible layout อยู่แล้ว
  • export เปิดเผยฟังก์ชัน Jam ด้วย plain unmangled name ตาม C calling convention
    • export fn counterAdd(c: mut Counter, n: i64) i64 เรียกจาก C ได้เป็น int64_t counterAdd(Counter *c, int64_t n);
    • parameter mut Counter ถูก lowering เป็น Counter * ไปยัง storage ที่ caller เป็นเจ้าของ
  • body ของฟังก์ชันฝั่ง Jam เป็น Jam ปกติ จึงยังใช้ drop, init analysis และ call-site exclusivity rule ต่อไป
  • ทิศทางการเรียกเข้า C ประกาศ C signature ด้วย extern
    • extern function ปฏิบัติตาม C ABI แบบ literal
    • parameter-mode machinery ไม่ถูกใช้ข้าม boundary
    • ส่ง buffer ไปให้ C ด้วย raw pointer และ Jam ไม่ตรวจสอบว่า C ทำอะไรกับ pointer นั้น
  • ขอบเขตที่ Jam ต้องการให้คือ ฝั่ง Jam ยังคง safe by default และเมื่อเปิดเผย Jam library เป็น C ABI ก็ไม่ต้องสร้าง unsafe API mirror หรือ shim layer แยก

Pattern matching

  • match ของ Jam อยู่ในรูป Pattern Block และไม่ใช้ =>
    • scrutinee ใช้วงเล็บเหมือน match (opcode)
    • _ เป็น arm แบบ catch-all
    • arm ทำงานแบบ top-to-bottom sequential first-match และไม่มี implicit fallthrough
  • opcode dispatcher ของ Game Boy emulator เป็น use case หลัก
    • เป็นรูปแบบ dispatch 256 base opcodes และ 256 prefix opcodes
  • รองรับ enum payload matching ด้วย
    • variant pattern match กับ tag และ bind payload field เป็น fresh local ภายใน arm
    • compiler ตรวจ exhaustiveness ของชุด variant
    • ถ้าเพิ่ม variant ใหม่ match site ที่ไม่ได้จัดการ variant นั้นจะ compile fail
  • match ทำงานเป็น expression ได้ด้วย
    • block ของแต่ละ arm สร้างค่าจาก trailing expression
    • arm ทุกตัวต้อง produce type เดียวกัน
    • match ต้อง exhaustive
  • ภายในนั้น match ทั้งหมดถูก compile ผ่าน decision tree pipeline อ้างอิง Luc Maranget 2008
    • integer literal cascade จะถูก LLVM simplifycfg fold เป็น switch และ jump table เมื่อคุ้มค่า

การออกแบบ compile time

  • compile pipeline ของ Rust ผ่าน IR และขั้นวิเคราะห์หลายชุด
    • tokens → AST → HIR → THIR → MIR → monomorphization → LLVM IR → machine code
    • trait solving เป็น search problem และ borrow checking เป็น whole-function region analysis
    • monomorphization เพิ่ม code volume ก่อนถึง LLVM
  • pipeline ของ Jam ถูกออกแบบให้สั้นกว่า
    • tokens → AST → AstGen → JIR → codegen → LLVM IR → machine code
    • ใช้ typed IR ตัวเดียวคือ JIR
  • JIR มี type ตั้งแต่ตอน AstGen สร้าง
    • มองว่า Jam ไม่มี comptime-as-values ที่บังคับให้ต้องทำ untyped lowering
    • drop placement, init-before-use check และ call-site exclusivity rule ทำด้วย local dataflow pass บน JIR
  • เพราะ binding ทุกตัวมี type annotation จึงมองว่าภาระของ global type inference และ open-ended trait search น้อยลง
  • AST และ JIR เป็น flat data structure
    • pack node ขนาดเล็กคงที่ลงใน contiguous array
    • ใช้ index แทน pointer และเก็บ oversized payload ใน side pool
    • ทำให้ compiler เดิน array ที่ cache-friendly แทนการติดตาม heap-allocated tree
  • ใน backend นั้น LLVM ครองเวลา optimization ของ release build
    • มีแผน split ให้ใช้ Cranelift สำหรับ debug build และ LLVM สำหรับ release build
    • Cranelift อยู่ใน roadmap และยังไม่เสร็จ
  • ปัจจุบัน compiler เป็น implementation ด้วย C++ ในขั้น bootstrap ภาษา และยังไม่มี build-time benchmark ที่อ้างอิงได้
    • claim เกี่ยวกับ compile-time เป็น design claim ไม่ใช่ผลการวัด

Runtime performance และตัวอย่าง

  • เป้าหมายคือให้ Jam มี performance ทัดเทียม Rust และ Zig
  • Jam ไม่มี GC, managed-memory runtime หรือ per-allocation header
    • codegen เป็น LLVM IR แบบตรงไปตรงมา
  • ยังไม่ได้ถือว่าถึงระดับ Rust และ Zig แล้ว
    • Rust และ Zig ทำงานมานานกับ target-specific intrinsic ของ standard library, auto-vectorization hint, allocator-aware container, hot path tuning และ LLVM pass tuning
    • Jam ก็ต้องทำงานลักษณะเดียวกันเพื่อบีบช่องว่าง 10–30% สุดท้าย
  • workload ที่วัดตอนนี้มองว่าช่องว่างอยู่ใน small constant factor ไม่ใช่ “คนละ class”
  • มี Tetris demo ที่รันใน terminal เขียนด้วย Jam

แผนเปิดเผยและงานที่เหลือ

  • Jam ยังไม่ public
    • compiler มีอยู่และทำงานได้ แต่ยังอยู่ก่อน wider release
  • กำลังดำเนินงานต่อไปนี้เพื่อ usability ในการใช้งานประจำวัน
    • stable surface
    • package manager
    • LSP
    • formatter
    • tooling ส่วนที่เหลือ
  • ยังมีหัวข้อที่จะเขียนแยกต่างหาก
    • parameter mode system
    • exclusivity rule
    • generics
    • comptime ของ Jam
    • standard library
    • allocator systems
    • panic model
    • MLIR exploration สำหรับ GPU codegen pipeline
    • Rust ABI work for FFI
    • Cranelift
    • เส้นทาง self-hosted compiler
  • แผนโอเพนซอร์สคือเปิดเผยหลังจากสร้าง 108 distinct project ด้วย Jam
    • เลข 108 เป็น arbitrary milestone ที่มาจาก 108 Stars of Destiny ใน Suikoden 2
    • ตอนนี้ปล่อยให้ small group of users แล้ว และมีแผนขยายวงเมื่อ tooling ตามทัน
  • early access รับได้ผ่าน beta list ที่ jamlang.org

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

 
GN⁺ 3 시간 전
ความคิดเห็นจาก Lobste.rs
  • การแก้คุณลักษณะเป็นปัญหาการค้นหา การตรวจสอบการยืมเป็นการวิเคราะห์ขอบเขตทั้งฟังก์ชัน การทำ monomorphization เพิ่มปริมาณโค้ดก่อนที่ LLVM ซึ่งเป็นขั้นตอนที่ช้าที่สุดจะได้เห็นเสียอีก…

    บทความที่สร้างโดย LLM แบบนี้กำลังทำสิ่งที่วิศวกร โดยเฉพาะวิศวกรรุ่นใหม่ ควรระวัง: ใช้ร้อยแก้วเชิงคุณภาพที่ฟังดูน่าเชื่อมาแทน ข้อมูลเชิงปริมาณ
    การโน้มน้าวด้วยเรื่องเล่าง่ายกว่าการรวบรวมและวิเคราะห์ตัวเลขที่หนักแน่น ทั้งสำหรับผู้เขียนและผู้อ่าน สมองมนุษย์ชอบเรื่องเล่า และเรื่องเล่าจะได้ผลดีที่สุดเมื่อมันเรียบง่ายและเป็นระเบียบ ข้อมูลจริงมักสะท้อนโลกที่ซับซ้อนและมีนัยละเอียดมากเท่าที่เรายอมก้มลงไปดู
    ลองเทียบกับบทความบล็อกเชิงปริมาณเรื่องการโปรไฟล์คอมไพเลอร์ Rust ที่เขียนโดยผู้ร่วมพัฒนา rustc ก็ได้

    • ผมเริ่มสงสัยตั้งแต่แรกแล้วว่าควรเอาโปรเจกต์นี้มองอย่างจริงจังหรือไม่
    • ผมชอบวลี “ใช้ร้อยแก้วเชิงคุณภาพ/เชิงปลุกเร้ามาแทนข้อมูลเชิงปริมาณ” เป็นพิเศษ
      งานเขียนทางเทคนิคที่ดีสามารถและควรมีทั้งสองอย่าง หากเหมาะสม แต่ต้องไม่พลาดสิ่งที่จำเป็นต้องสื่อจริง ๆ พอได้บริหารองค์กรงานประกันขนาดใหญ่จึงรู้ว่างานเขียนทางเทคนิคนั้นยากแค่ไหน และเมื่อ การเข้าถึง LLM ง่ายขึ้น ก็ต้องระวังว่าปัญหานี้อาจแย่ลงได้อีกมากเพียงใด
  • ความแตกต่างหลักกับ Zig คือมี drop และไม่มีองค์ประกอบบางอย่างที่ถูกใช้ผิดได้ง่ายอย่าง undefined ใช่ไหม?

    ไม่มี undefined และค่าทั้งหมดต้องถูกกำหนดค่าเริ่มต้น แต่ Maybe(T).empty() ส่งคืนค่าที่เนื้อหา “ยังไม่มีความหมาย” และถ้าเรียก unsafeAssumeInit() ต่อทันที ก็น่าจะคืนค่าขยะออกมา ดังนั้นมันจึงไม่ใช่ความปลอดภัยในความหมายแบบ Rust ที่คอมไพเลอร์ถือว่า unsafe เป็นมลทินที่ต้องมี unsafe { .. } อย่างชัดเจน

    ตัวอย่างที่แสดงความปลอดภัยและฟีเจอร์ drop คือโค้ดนี้:

    const File = struct {  
        fd: i32,  
        fn drop(self: mut File) {  
            close(self.fd);  
        }  
    };
    
    export fn useFile() i32 {  
        const f: File = { fd: 7 };  
        return f.fd;  
    }  
    

    ถ้าผมไม่ได้ดูผิด นี่ไม่ปลอดภัยหรือเปล่า? ต่อให้มองข้ามการกำหนด file descriptor ด้วยมือไป หลังจากเรียก close(7) แล้วก็คืนค่า 7 ออกมา เพราะไม่มี การติดตามอายุการใช้งาน ผู้ใช้จึงไม่มีวิธีบอกว่าอายุการใช้งานของ file descriptor สิ้นสุดก่อนที่ useFile() จะ return

    ในตัวอย่าง ABI เมื่อ export fn counterAdd(c: mut Counter, n: i64) i64 { .. } กลายเป็น int64_t counterAdd(Counter *c, int64_t n); จะสื่อได้อย่างไรว่า c เป็น NULL ได้หรือไม่ได้? Rust มี ABI ที่กำหนดไว้สำหรับส่วนนี้ และทำได้ทั้ง extern "C" fn counterAdd(c: &mut Counter, n: i64) -> i64 และ extern "C" fn counterAdd(c: Option<&mut Counter>, n: i64) -> i64

    เวอร์ชัน Rust ก็ไม่ต้องใช้ unsafe เช่นกัน สามารถกำหนด API ด้วย reference ได้ ที่ย้อนแย้งคือจุดเดียวที่อาจต้องใช้ unsafe ใน Rust รุ่นใหม่คือ #[no_mangle] ซึ่งตอนนี้เป็น #[unsafe(no_mangle)] แต่ตัวอย่างกลับจัดให้ฝั่ง Rust ใช้ raw pointer ด้วยเหตุผลบางอย่าง

    ตัวอย่างช่วงท้ายนี้ก็เช่นกัน:

    extern fn snprintf(buf: *mut[] u8, size: u64, fmt: *const[] u8, ...) i32;
    
    fn render(value: i32) i32 {  
        var buf: [16]u8 = [0; 16];  
        return snprintf(&buf[0], 16, "n=%d", value);  
    }  
    

    ตรงไหนสักแห่งไม่ควรมี unsafe หรือ? ในเมื่อ snprintf รับ raw pointer ถ้าตามแนวทางที่บอกไว้ก่อนหน้าว่างาน unsafe ควรถูกค้นหาได้จากชื่อ ก็น่าจะต้องมีอะไรอย่าง unsafeSnprintf และการกำหนด symbol ใหม่

    “เบาะแสที่ซื่อตรงข้อหนึ่ง: ในบรรทัด extern คุณกำลังคุยกับ C และกฎของ C เป็นฝ่ายชนะ” นี่ก็ อืม

    • ผมก็อ่านแบบนั้นเหมือนกัน แต่ก็ไม่ต่างจาก .as_raw_fd() ของ Rust และตรงนั้นก็มี ปัญหาด้านความปลอดภัย แบบเดียวกัน
  • สิ่งที่ทำให้ ABI ของ Rust ไม่เสถียรนั้นไม่มีอยู่ใน Jam ไม่มี reference ระดับ first-class ไม่มี lifetime ไม่มีเลย์เอาต์แบบ niche-packed ที่ต้องลบออก

    นี่เป็นการเข้าใจผิดเรื่อง ความเสถียรของ FFI ในไลบรารีมาตรฐาน Rust shared reference, mutable reference, Box และ Option ของสิ่งเหล่านั้น ล้วนมี ABI ที่กำหนดไว้และเสถียร ดังนั้นกระบวนการ Box::into_raw/from_raw ทั้งหมดในตัวอย่างจึงไม่จำเป็น
    lifetime ไม่มีอยู่เลยในระดับไบนารี ถ้าเลือกว่าจะกำหนด ABI ที่เสถียรให้ enum การปรับแต่งแบบ niche optimization ก็จะถูกปิดใช้

    เหตุผลที่ type ส่วนใหญ่ไม่ได้กำหนด ABI ที่เสถียร คือบ่อยครั้งเราไม่ต้องการ ABI ที่เสถียร เพราะมันจะทำให้เปลี่ยนภายในของ type ไม่ได้

  • Jam ยังไม่ได้เปิดเผยต่อสาธารณะ คอมไพเลอร์มีอยู่และรันได้ แต่เรากำลังเลื่อนการเปิดเผยในวงกว้างออกไป ระหว่างทำสิ่งต่าง ๆ ที่ทำให้ใช้งานทุกวันได้ดี—พื้นผิวที่เสถียร, package manager, LSP, formatter และเครื่องมืออื่น ๆ ที่คุณจะสังเกตเห็นก็ต่อเมื่อมันไม่มี…

    ผมไม่เข้าใจการเลือกนี้ มีความแตกต่างอย่างมากระหว่างการ “ปล่อย” สิ่งที่ยังไม่สมบูรณ์กับการแค่ เปิดซอร์ส ถ้าสุดท้ายก็จะทำอยู่แล้ว การเปิดเผยระหว่างสร้างโปรเจกต์จะเสียหายตรงไหน?
    ข้อดีคือคนที่ชอบทิศทางนี้สามารถลองใช้เอง และอาจถึงขั้นมีส่วนร่วมได้ แน่นอนว่าใน “ยุคของ AI” ก็ยังไม่ชัดว่าการมีส่วนร่วมนั้นจะเป็นผลบวกสุทธิหรือไม่ อีกทั้งยังช่วยให้ผู้คนเข้าใจได้ดีขึ้นว่ากำลังสร้างอะไร และประเมินข้ออ้างว่ามันยอดเยี่ยมเพราะอะไรได้ ถ้าทำสิ่งนั้นไม่ได้ โปรเจกต์ก็จะน่าสนใจน้อยลงมาก

    ยิ่งไปกว่านั้น ยังมีคนที่ไม่ได้ใช้เครื่องมือเหล่านี้เลยด้วย ตอนนี้ทีมของผมเองยังตกลงกันไม่ได้ด้วยซ้ำว่าจะนำ formatter อัตโนมัติมาใช้ แต่ในด้านอื่น ๆ ก็ยอดเยี่ยม ดังนั้นการเลื่อนการเปิดเผยออกไประหว่างสร้างเครื่องมือเหล่านั้นจึงไม่ได้สร้างความแตกต่างมากนัก

  • ผู้คนพยายามสร้าง “Rust ที่ไม่มี lifetime อันน่ารำคาญ” กันอยู่เรื่อย ๆ และก็ล้มเหลวอยู่เรื่อย ๆ คอมเมนต์อื่นพูดถึงรูปแบบความล้มเหลวแบบหนึ่งไปแล้ว คือปัญหาการคืนค่าบางส่วนของค่าที่ถูก drop เกิดขึ้นเพราะไม่สามารถคืนค่า reference ได้ อีกปัญหาคลาสสิกคือแบบนี้:

    let mut arr = vec![1];  
    let x = &arr[0];  
    arr.push(2);  
    // `x`를 사용하면 어떻게 되나?  
    

    คำตอบมีสามแบบ:

    1. ปฏิเสธ ถ้าจะทำแบบนั้นก็จำเป็นต้องมีแนวคิดเรื่องการ borrow ในรูปแบบใดรูปแบบหนึ่ง โดยปกติคือ shared XOR mutable แต่ถ้ามีแค่ mutable ก็ไม่สะดวก และถ้ามีแค่ shared ก็ไม่ปลอดภัย
    2. อนุญาต เพราะไม่มี reference ผ่านตัวแปรอื่นอยู่ และทุกอย่างเป็น GC หรือ pointer แบบ reference-counted
    3. อนุญาต แล้วทำให้เกิด undefined behavior ตอน runtime

    ไม่ว่าจะเลือกข้อไหนในสามข้อนี้ก็มีเหตุผลที่ดีในแบบของมัน แต่ Jam ดูเหมือนอยากเป็นข้อ 1 แบบ Rust ทว่าในทางปฏิบัติเพราะ value semantics เลยดูเหมือนเป็นข้อ 2 ถ้านั่นหมายความว่าทุกอย่างถูกคัดลอก ก็อาจทำให้เขียนโครงสร้างข้อมูลที่ทั้งปลอดภัยและมีประสิทธิภาพไม่ได้

    • ผมคิดว่า Inko ทำได้ค่อนข้างดี แน่นอนว่าต้องหักลบอคติที่ชัดเจนของผมออกไป แต่ตัวมันเองก็มี trade-off ที่ชัดเจนเหมือนกัน
      โดยเฉพาะถ้าทิ้ง borrow checker ไป การรองรับ type ที่ allocate บน stack จะยากขึ้นมาก หากไม่ใส่เบาะแสหลายอย่างเข้ามา เช่น วิธีคัดลอกตอน borrow ซึ่งทั้ง Inko และ Swift ต่างก็ทำแบบนี้
    • ผมไม่ค่อยรู้เรื่อง Jam แต่ mutable value semantics แบบ Hylo มีรูปแบบการ borrow ที่เรียกว่า subscripts ดังนั้นจึงใกล้เคียงกับพื้นที่ตรงกลางมากกว่า
    • ตอนอ่านส่วนนั้น คำถามแรกที่ผุดขึ้นมาคือ “ถ้าไม่มี reference และไม่มี annotation ของ lifetime แล้วจะเก็บ reference ไว้ใน struct ได้อย่างไร?”
      ดู language reference แล้วไม่มี reference แต่มี pointer แบบ mut และ const และผมหาเนื้อหาเกี่ยวกับความปลอดภัยของมันไม่เจอ
  • องค์ประกอบใหญ่ที่ทำให้ Zig เป็น Zig คือ การไม่มี RAII ส่วน Rust คือ borrow checker แต่ผมไม่ค่อยแน่ใจว่าจริง ๆ แล้วใครต้องการ “RAII ที่ไม่มี reference” ซึ่งเป็นจุดที่ตัวเลือกการออกแบบเหล่านี้ไปถึง
    ถึงอย่างนั้นผมก็คิดว่ายังมีพื้นที่ให้ทดลองในช่องว่างนี้ และมองความพยายามแบบนั้นในแง่ดี เพียงแต่ไม่คิดว่าแนวทางนี้ใช่

    ทิศทางที่ผมคิดอยู่เรื่อย ๆ ช่วงนี้คือการผสมกันของ comptime ของ Zig, สิทธิ์ของ reference ที่คล้าย Pony, การปฏิบัติกับ lifetime เป็นค่า compile-time และการ branding lifetime เข้ากับ allocator
    สิ่งที่คาดหวังคือการเพิ่ม reference safety ให้กับกลยุทธ์ allocator ของ Zig และได้ lifetime ที่แทบไม่ต้องเขียน annotation

  • ภาษาใหม่ ๆ เป็นเรื่องดี แต่ไม่ชอบที่ทุกอย่างกลายเป็น frontend ของ LLVM เข้าใจว่า backend นั้นยาก แต่บางครั้งก็อยากให้มี ทางเลือกอื่น บ้าง

  • ฟังดูแทบจะเหมือน Swift