ภาษาโปรแกรม Jam
(rapha.land)- 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
- ไม่มี explicit cleanup,
- ชื่อแบบ 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 ทุกแบบ
- แนวทางคือหลีกเลี่ยงความซับซ้อนอย่าง rule of 0/3/5 ของ C++, virtual destructor, constructor exception, destructor exception,
การ 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
- ใน Debug mode จะเติม
- Go initialize
varทุกตัวเป็น zero เพื่อกัน garbage read แต่มีต้นทุนในการเขียน zero pattern แม้กับ field ที่กำลังจะถูก overwrite ทันที - Jam หลีกเลี่ยงทั้ง
undefinedและ implicit zero - สำหรับ lazy initialization และ out-parameter ใช้
Maybe(T)empty()สร้าง slot ที่มี contents ยังไม่มีความหมายwrite()เติมค่าใน slotunsafeAssumeInit()ดึงค่าออกมา
- lint pass ติดตามว่า slot ถูก write แล้วหรือไม่ และปฏิเสธการเรียก
unsafeAssumeInit()ที่ analyzer พิสูจน์ initialization ไม่ได้เป็น compile error- prefix
unsafeยังคงเป็น anchor ที่ human และ AI reviewer สามารถ grep ได้
- prefix
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 ถัดไป
- binding ภายใน
- ในตัวอย่าง 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]เป็น getterv.at(i)ที่คืน element เป็น value
- call site exclusivity check ตรวจสอบ path overlap ของ borrow set ที่ argument สร้างขึ้น
swap(p.x, p.y)เป็น sub-path ที่ disjoint จึง OKmoveX(p, p.x)เป็น error เพราะpกับp.xoverlap กัน
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 นี้
- การ dereference raw pointer เป็น
- 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 conventionexport 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 ด้วย
externexternfunction ปฏิบัติตาม 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
- scrutinee ใช้วงเล็บเหมือน
- 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
simplifycfgfold เป็นswitchและ jump table เมื่อคุ้มค่า
- integer literal cascade จะถูก LLVM
การออกแบบ 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 ความคิดเห็น
ความคิดเห็นจาก Lobste.rs
บทความที่สร้างโดย LLM แบบนี้กำลังทำสิ่งที่วิศวกร โดยเฉพาะวิศวกรรุ่นใหม่ ควรระวัง: ใช้ร้อยแก้วเชิงคุณภาพที่ฟังดูน่าเชื่อมาแทน ข้อมูลเชิงปริมาณ
การโน้มน้าวด้วยเรื่องเล่าง่ายกว่าการรวบรวมและวิเคราะห์ตัวเลขที่หนักแน่น ทั้งสำหรับผู้เขียนและผู้อ่าน สมองมนุษย์ชอบเรื่องเล่า และเรื่องเล่าจะได้ผลดีที่สุดเมื่อมันเรียบง่ายและเป็นระเบียบ ข้อมูลจริงมักสะท้อนโลกที่ซับซ้อนและมีนัยละเอียดมากเท่าที่เรายอมก้มลงไปดู
ลองเทียบกับบทความบล็อกเชิงปริมาณเรื่องการโปรไฟล์คอมไพเลอร์ Rust ที่เขียนโดยผู้ร่วมพัฒนา rustc ก็ได้
งานเขียนทางเทคนิคที่ดีสามารถและควรมีทั้งสองอย่าง หากเหมาะสม แต่ต้องไม่พลาดสิ่งที่จำเป็นต้องสื่อจริง ๆ พอได้บริหารองค์กรงานประกันขนาดใหญ่จึงรู้ว่างานเขียนทางเทคนิคนั้นยากแค่ไหน และเมื่อ การเข้าถึง LLM ง่ายขึ้น ก็ต้องระวังว่าปัญหานี้อาจแย่ลงได้อีกมากเพียงใด
ความแตกต่างหลักกับ Zig คือมี
dropและไม่มีองค์ประกอบบางอย่างที่ถูกใช้ผิดได้ง่ายอย่างundefinedใช่ไหม?ไม่มี
undefinedและค่าทั้งหมดต้องถูกกำหนดค่าเริ่มต้น แต่Maybe(T).empty()ส่งคืนค่าที่เนื้อหา “ยังไม่มีความหมาย” และถ้าเรียกunsafeAssumeInit()ต่อทันที ก็น่าจะคืนค่าขยะออกมา ดังนั้นมันจึงไม่ใช่ความปลอดภัยในความหมายแบบ Rust ที่คอมไพเลอร์ถือว่าunsafeเป็นมลทินที่ต้องมีunsafe { .. }อย่างชัดเจนตัวอย่างที่แสดงความปลอดภัยและฟีเจอร์
dropคือโค้ดนี้:ถ้าผมไม่ได้ดูผิด นี่ไม่ปลอดภัยหรือเปล่า? ต่อให้มองข้ามการกำหนด 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 ด้วยเหตุผลบางอย่างตัวอย่างช่วงท้ายนี้ก็เช่นกัน:
ตรงไหนสักแห่งไม่ควรมี
unsafeหรือ? ในเมื่อsnprintfรับ raw pointer ถ้าตามแนวทางที่บอกไว้ก่อนหน้าว่างานunsafeควรถูกค้นหาได้จากชื่อ ก็น่าจะต้องมีอะไรอย่างunsafeSnprintfและการกำหนด symbol ใหม่“เบาะแสที่ซื่อตรงข้อหนึ่ง: ในบรรทัด
externคุณกำลังคุยกับ C และกฎของ C เป็นฝ่ายชนะ” นี่ก็ อืม.as_raw_fd()ของ Rust และตรงนั้นก็มี ปัญหาด้านความปลอดภัย แบบเดียวกันนี่เป็นการเข้าใจผิดเรื่อง ความเสถียรของ FFI ในไลบรารีมาตรฐาน Rust shared reference, mutable reference,
BoxและOptionของสิ่งเหล่านั้น ล้วนมี ABI ที่กำหนดไว้และเสถียร ดังนั้นกระบวนการBox::into_raw/from_rawทั้งหมดในตัวอย่างจึงไม่จำเป็นlifetime ไม่มีอยู่เลยในระดับไบนารี ถ้าเลือกว่าจะกำหนด ABI ที่เสถียรให้ enum การปรับแต่งแบบ niche optimization ก็จะถูกปิดใช้
เหตุผลที่ type ส่วนใหญ่ไม่ได้กำหนด ABI ที่เสถียร คือบ่อยครั้งเราไม่ต้องการ ABI ที่เสถียร เพราะมันจะทำให้เปลี่ยนภายในของ type ไม่ได้
ผมไม่เข้าใจการเลือกนี้ มีความแตกต่างอย่างมากระหว่างการ “ปล่อย” สิ่งที่ยังไม่สมบูรณ์กับการแค่ เปิดซอร์ส ถ้าสุดท้ายก็จะทำอยู่แล้ว การเปิดเผยระหว่างสร้างโปรเจกต์จะเสียหายตรงไหน?
ข้อดีคือคนที่ชอบทิศทางนี้สามารถลองใช้เอง และอาจถึงขั้นมีส่วนร่วมได้ แน่นอนว่าใน “ยุคของ AI” ก็ยังไม่ชัดว่าการมีส่วนร่วมนั้นจะเป็นผลบวกสุทธิหรือไม่ อีกทั้งยังช่วยให้ผู้คนเข้าใจได้ดีขึ้นว่ากำลังสร้างอะไร และประเมินข้ออ้างว่ามันยอดเยี่ยมเพราะอะไรได้ ถ้าทำสิ่งนั้นไม่ได้ โปรเจกต์ก็จะน่าสนใจน้อยลงมาก
ยิ่งไปกว่านั้น ยังมีคนที่ไม่ได้ใช้เครื่องมือเหล่านี้เลยด้วย ตอนนี้ทีมของผมเองยังตกลงกันไม่ได้ด้วยซ้ำว่าจะนำ formatter อัตโนมัติมาใช้ แต่ในด้านอื่น ๆ ก็ยอดเยี่ยม ดังนั้นการเลื่อนการเปิดเผยออกไประหว่างสร้างเครื่องมือเหล่านั้นจึงไม่ได้สร้างความแตกต่างมากนัก
ผู้คนพยายามสร้าง “Rust ที่ไม่มี lifetime อันน่ารำคาญ” กันอยู่เรื่อย ๆ และก็ล้มเหลวอยู่เรื่อย ๆ คอมเมนต์อื่นพูดถึงรูปแบบความล้มเหลวแบบหนึ่งไปแล้ว คือปัญหาการคืนค่าบางส่วนของค่าที่ถูก
dropเกิดขึ้นเพราะไม่สามารถคืนค่า reference ได้ อีกปัญหาคลาสสิกคือแบบนี้:คำตอบมีสามแบบ:
ไม่ว่าจะเลือกข้อไหนในสามข้อนี้ก็มีเหตุผลที่ดีในแบบของมัน แต่ Jam ดูเหมือนอยากเป็นข้อ 1 แบบ Rust ทว่าในทางปฏิบัติเพราะ value semantics เลยดูเหมือนเป็นข้อ 2 ถ้านั่นหมายความว่าทุกอย่างถูกคัดลอก ก็อาจทำให้เขียนโครงสร้างข้อมูลที่ทั้งปลอดภัยและมีประสิทธิภาพไม่ได้
โดยเฉพาะถ้าทิ้ง borrow checker ไป การรองรับ type ที่ allocate บน stack จะยากขึ้นมาก หากไม่ใส่เบาะแสหลายอย่างเข้ามา เช่น วิธีคัดลอกตอน borrow ซึ่งทั้ง Inko และ Swift ต่างก็ทำแบบนี้
ดู 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