Every surviving carrier now owns its artifacts: checked mkdir/mkdtemp/ fopen acquisition, one all-exit cleanup funnel per carrier, ENOENT- tolerant checked unlinks, exact-path deletion (rm -rf only for an owned pid-keyed dir or a .sepwork beneath one), and cleanup failure fails a passing carrier without overwriting its diagnostic. In the same pass the carriers adapt to the driver contract this branch lands: --sep and WW_PKGCACHE are gone, -S and the /tmp/ww_run_<pid> scratch contract are asserted, and rows whose runtime or reject coverage moved to test/wcc/data fixtures or test/lang @test owners are trimmed to the byte/artifact/diagnostic observations only they can make. Repair and adaptation ride together because most files interleave both in the same hunks; splitting would manufacture intermediate carrier states that never existed and cannot run against either driver.
1888 lines
65 KiB
C
1888 lines
65 KiB
C
/*
|
|
* 700_e2e — end-to-end. Drive `ww build` on a small source program,
|
|
* run the produced binary, check the exit status. This is the real
|
|
* user-facing happy path.
|
|
*/
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <errno.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/wait.h>
|
|
#include "wwtestpkg.h"
|
|
|
|
static int
|
|
runwait(const char *cmd)
|
|
{
|
|
int rc = system(cmd);
|
|
if (rc == -1) return -1;
|
|
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
|
|
return 1;
|
|
}
|
|
|
|
struct row { const char *src; int want_exit; };
|
|
|
|
static const struct row rows[] = {
|
|
{ "fn main() i32 = { return 42; };", 42 },
|
|
{ "fn add(a: i32, b: i32) i32 = { return a + b; };\n"
|
|
"fn main() i32 = { return add(7, 35); };", 42 },
|
|
{ "fn main() i32 = {\n"
|
|
" let i: i32 = 0;\n"
|
|
" let s: i32 = 0;\n"
|
|
" for (i < 10) { s += i; i += 1; };\n"
|
|
" return s;\n"
|
|
"};", 45 },
|
|
{ "fn main() i32 = {\n"
|
|
" let x: i32 = 100;\n"
|
|
" if (x > 50) { return 1; };\n"
|
|
" return 0;\n"
|
|
"};", 1 },
|
|
{ "fn main() i32 = {\n"
|
|
" let a: i32 = 6;\n"
|
|
" let b: i32 = 7;\n"
|
|
" return a * b;\n"
|
|
"};", 42 },
|
|
/* multi-return tuple, divmod */
|
|
{ "fn divmod(a: i64, b: i64) (i64, i64) = { return a / b, a % b; };\n"
|
|
"fn main() i32 = {\n"
|
|
" let q, r = divmod(17, 5);\n"
|
|
" return (q + r): i32;\n"
|
|
"};", 5 },
|
|
/* fixed array, byte-wise read/write */
|
|
{ "fn main() i32 = {\n"
|
|
" let buf: [4]u8;\n"
|
|
" buf[0] = 1: u8;\n"
|
|
" buf[1] = 2: u8;\n"
|
|
" buf[2] = 3: u8;\n"
|
|
" buf[3] = 4: u8;\n"
|
|
" let sum: i32 = 0;\n"
|
|
" let i: i32 = 0;\n"
|
|
" for (i < 4) { sum += buf[i]: i32; i += 1; };\n"
|
|
" return sum;\n"
|
|
"};", 10 },
|
|
/* float: arg, arith, literal, cast back to int */
|
|
{ "fn area(r: f64) f64 = { return 3.14 * r * r; };\n"
|
|
"fn main() i32 = { let a: f64 = area(5.0); return a: i32; };", 78 },
|
|
/* float comparison: must emit UCOMISD + JA (not CMPQ + JG) */
|
|
{ "fn main() i32 = {\n"
|
|
" let a: f64 = 1.5;\n"
|
|
" let b: f64 = 2.5;\n"
|
|
" if (a < b) { if (b > a) { return 7; }; };\n"
|
|
" return 0;\n"
|
|
"};", 7 },
|
|
/* float ==/!= via UCOMISD */
|
|
{ "fn main() i32 = {\n"
|
|
" let a: f64 = 3.14;\n"
|
|
" let b: f64 = 3.14;\n"
|
|
" if (a == b) { return 11; };\n"
|
|
" return 0;\n"
|
|
"};", 11 },
|
|
/* function pointer: take address of a named fn, call indirectly */
|
|
{ "fn add(a: i32, b: i32) i32 = { return a + b; };\n"
|
|
"fn main() i32 = {\n"
|
|
" let fp: fn(a: i32, b: i32) i32 = add;\n"
|
|
" return fp(20, 22);\n"
|
|
"};", 42 },
|
|
/* string literal via syscall — exit code = bytes written */
|
|
{ "@symbol(\"rt_syscall\") fn rt_syscall(num: i64, a: i64, b: i64, c: i64) i64;\n"
|
|
"fn print(s: str) i64 = { return rt_syscall(1, 1, s.ptr: i64, s.len: i64); };\n"
|
|
"fn main() i32 = { return print(\"hello, world\\n\"): i32; };", 13 },
|
|
/* 9 args — 3 spill to the stack */
|
|
{ "fn s9(a: i32, b: i32, c: i32, d: i32, e: i32, f: i32, g: i32, h: i32, i: i32) i32 = {\n"
|
|
" return a + b + c + d + e + f + g + h + i;\n"
|
|
"};\n"
|
|
"fn main() i32 = { return s9(1,2,3,4,5,6,7,8,9); };", 45 },
|
|
/* defer: LIFO at function return */
|
|
{ "@symbol(\"rt_syscall\") fn rt_syscall(num: i64, a: i64, b: i64, c: i64) i64;\n"
|
|
"fn out(c: i32) void = { rt_syscall(1, 1, (&c): i64, 1); };\n"
|
|
"fn main() i32 = {\n"
|
|
" let c1: i32 = 0;\n"
|
|
" let c2: i32 = 0;\n"
|
|
" c1 = 65;\n" /* 'A' */
|
|
" c2 = 66;\n" /* 'B' */
|
|
" defer out(c1);\n"
|
|
" defer out(c2);\n"
|
|
" return 0;\n"
|
|
"};", 0 },
|
|
/* struct-by-value: 16B all-int passed by value */
|
|
{ "type pair = struct { a: i64, b: i64 };\n"
|
|
"fn sum(p: pair) i64 = { return p.a + p.b; };\n"
|
|
"fn main() i32 = {\n"
|
|
" let p: pair = pair { a = 10, b = 32 };\n"
|
|
" return sum(p): i32;\n"
|
|
"};", 42 },
|
|
/* f32 cast + arithmetic */
|
|
{ "fn add32(a: f32, b: f32) f32 = { return a + b; };\n"
|
|
"fn main() i32 = {\n"
|
|
" let r: f32 = add32(2.5: f32, 7.5: f32);\n"
|
|
" return r: i32;\n"
|
|
"};", 10 },
|
|
/* slice from array: build header, iterate via index/len */
|
|
{ "fn main() i32 = {\n"
|
|
" let arr: [4]u8;\n"
|
|
" arr[0] = 10: u8; arr[1] = 20: u8;\n"
|
|
" arr[2] = 30: u8; arr[3] = 99: u8;\n"
|
|
" let s: []u8 = arr[0:3];\n"
|
|
" let sum: i32 = 0;\n"
|
|
" let i: i32 = 0;\n"
|
|
" for (i < s.len) { sum += s[i]: i32; i += 1; };\n"
|
|
" return sum;\n"
|
|
"};", 60 },
|
|
/* module imports: use os and call os.write; exit code = bytes */
|
|
{ "import os;\n"
|
|
"fn main() i32 = { return os.write(1, \"ok\\n\".ptr, 3): i32; };", 3 },
|
|
/* typed integer literals */
|
|
{ "fn main() i32 = {\n"
|
|
" let buf: [4]u8;\n"
|
|
" buf[0] = 65u8; buf[1] = 66u8; buf[2] = 67u8; buf[3] = 0u8;\n"
|
|
" let s: i32 = 0;\n"
|
|
" let i: i32 = 0;\n"
|
|
" for (i < 3) { s += buf[i]: i32; i += 1; };\n"
|
|
" return s;\n"
|
|
"};", 198 },
|
|
/* full stdlib stack: use os + strconv, str return, write
|
|
* the formatted number to stdout. exit code = number length. */
|
|
{ "import os;\n"
|
|
"import strconv;\n"
|
|
"fn main() i32 = {\n"
|
|
" let s: str = strconv.i64tos(12345, strconv.base.DEC);\n"
|
|
" os.write(1, s.ptr, s.len: u64);\n"
|
|
" os.write(1, \"\\n\".ptr, 1u64);\n"
|
|
" return s.len;\n"
|
|
"};", 5 },
|
|
/* alloc + free via mmap-backed runtime — write through allocated
|
|
* memory and free it. exit = 0 if the allocation succeeded. */
|
|
{ "import os;\n"
|
|
"import rt;\n"
|
|
"fn main() i32 = {\n"
|
|
" let p: *void = rt.malloc(4096u64);\n"
|
|
" if (p == nil) { return 1; };\n"
|
|
" let bp: *u8 = p: *u8;\n"
|
|
" bp[0] = 65u8;\n"
|
|
" os.write(1, bp, 1u64);\n"
|
|
" os.free(p, 4096u64);\n"
|
|
" return 0;\n"
|
|
"};", 0 },
|
|
/* argv: kernel passes argc in DI, argv in SI. */
|
|
{ "fn main(argc: i32, argv: **u8) i32 = { return argc; };", 1 },
|
|
/* i64 array: scaled indexing (elem size 8) */
|
|
{ "fn main() i32 = {\n"
|
|
" let arr: [4]i64;\n"
|
|
" arr[0] = 10; arr[1] = 20; arr[2] = 30; arr[3] = 40;\n"
|
|
" let sum: i64 = 0;\n"
|
|
" let i: i32 = 0;\n"
|
|
" for (i < 4) { sum += arr[i]; i += 1; };\n"
|
|
" return sum: i32;\n"
|
|
"};", 100 },
|
|
/* break out of an infinite loop early */
|
|
{ "fn main() i32 = {\n"
|
|
" let i: i32 = 0;\n"
|
|
" for () {\n"
|
|
" i += 1;\n"
|
|
" if (i == 7) { break; };\n"
|
|
" };\n"
|
|
" return i;\n"
|
|
"};", 7 },
|
|
/* switch with multi-expr cases + default */
|
|
{ "fn classify(x: i32) i32 = {\n"
|
|
" switch (x) {\n"
|
|
" case 1, 2, 3: return 10;\n"
|
|
" case 10: return 99;\n"
|
|
" case: return 50;\n"
|
|
" };\n"
|
|
" return -1;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" return classify(2) + classify(10) + classify(99);\n"
|
|
"};", 159 }, /* 10+99+50=159 */
|
|
/* fmt module: stdlib formatter for strings; ints compose
|
|
* via strconv.i64tos. */
|
|
{ "import fmt;\n"
|
|
"import strconv;\n"
|
|
"fn main() i32 = {\n"
|
|
" fmt.println(\"ww\");\n"
|
|
" fmt.println(strconv.i64tos(42, strconv.base.DEC));\n"
|
|
" fmt.println(strconv.i64tos(-7, strconv.base.DEC));\n"
|
|
" return 0;\n"
|
|
"};", 0 },
|
|
/* struct with i32 fields: MOVL/MOVSXD avoids clobbering neighbors */
|
|
{ "type point = struct { x: i32, y: i32 };\n"
|
|
"fn distsq(p: point) i32 = { return p.x * p.x + p.y * p.y; };\n"
|
|
"fn main() i32 = {\n"
|
|
" let p: point = point { x = 3, y = 4 };\n"
|
|
" return distsq(p);\n"
|
|
"};", 25 },
|
|
/* str equality via == and != */
|
|
{ "fn main() i32 = {\n"
|
|
" let a: str = \"hello\";\n"
|
|
" let b: str = \"hello\";\n"
|
|
" let c: str = \"world\";\n"
|
|
" let n: i32 = 0;\n"
|
|
" if (a == b) { n += 10; };\n"
|
|
" if (a != c) { n += 20; };\n"
|
|
" return n;\n"
|
|
"};", 30 },
|
|
/* CLAUDE.md's move pattern: ptr-to-struct compound field write */
|
|
{ "type point = struct { x: i32, y: i32 };\n"
|
|
"fn move(p: *point, dx: i32, dy: i32) void = {\n"
|
|
" p.x += dx; p.y += dy;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let pt: point = point { x = 0, y = 0 };\n"
|
|
" move(&pt, 3, 4);\n"
|
|
" return pt.x + pt.y;\n"
|
|
"};", 7 },
|
|
/* vtable polymorphism: struct of fn pointers, indirect call */
|
|
{ "type ops = struct { add: fn(a: i32, b: i32) i32 };\n"
|
|
"fn plus(a: i32, b: i32) i32 = { return a + b; };\n"
|
|
"fn main() i32 = {\n"
|
|
" let v: ops = ops { add = plus };\n"
|
|
" return v.add(20, 22);\n"
|
|
"};", 42 },
|
|
/* compound bitwise/shift assigns */
|
|
{ "fn main() i32 = {\n"
|
|
" let x: i32 = 100;\n"
|
|
" x &= 0x3f; x |= 0x80; x ^= 0xc4;\n"
|
|
" x *= 2; x <<= 1; x >>= 2;\n"
|
|
" return x;\n"
|
|
"};", 96 },
|
|
/* Hare-style builtins: append(s, v) and len(s). The compiler
|
|
* lowers append to a CALL into libwwrt.a's appendu8 / appendi64
|
|
* by element size — no `use rt;` or `use slices;` required. */
|
|
{ "import os;\n"
|
|
"fn main() i32 = {\n"
|
|
" let s: []u8;\n"
|
|
" s.ptr = nil; s.len = 0; s.cap = 0;\n"
|
|
" append(s, 88u8); append(s, 89u8); append(s, 90u8);\n"
|
|
" os.write(1, s.ptr, len(s): u64);\n"
|
|
" os.write(1, \"\\n\".ptr, 1u64);\n"
|
|
" return len(s);\n"
|
|
"};", 3 },
|
|
/* str-returning function: 16-byte return via AX:DX (SysV). The
|
|
* caller's str slot is filled from those two regs. */
|
|
{ "import strings;\n"
|
|
"import fmt;\n"
|
|
"fn main() i32 = {\n"
|
|
" let r: str = strings.concat(\"hello, \", \"world\");\n"
|
|
" fmt.println(r);\n"
|
|
" return r.len;\n"
|
|
"};", 12 },
|
|
/* variadic append + static qualifier (Hare idiom) */
|
|
{ "import os;\n"
|
|
"fn main() i32 = {\n"
|
|
" let s: []u8;\n"
|
|
" s.ptr = nil; s.len = 0; s.cap = 0;\n"
|
|
" static append(s, 72u8, 105u8, 33u8, 10u8);\n"
|
|
" os.write(1, s.ptr, len(s): u64);\n"
|
|
" return len(s);\n"
|
|
"};", 4 },
|
|
/* alloc() builtin: heap-allocate a struct, init from struct-lit.
|
|
* `package main;` is required so the bare-alloc-builtin gate
|
|
* (task #23) sees c->cur_mod=="main" and doesn't suppress the
|
|
* builtin via the inherited os.alloc decl. Task #30 graduated
|
|
* the builtin to `(*T | nomem)`; the `!` aborts on OOM. */
|
|
{ "package main;\n"
|
|
"import rt;\n"
|
|
"type point = struct { x: i32, y: i32 };\n"
|
|
"fn main() i32 = {\n"
|
|
" let p: *point = alloc(point { x = 3, y = 4 })!;\n"
|
|
" return p.x * p.x + p.y * p.y;\n"
|
|
"};", 25 },
|
|
/* Hare-style range loop: for (let x .. slice) iterates elements */
|
|
{ "import os;\n"
|
|
"fn main() i32 = {\n"
|
|
" let s: []u8;\n"
|
|
" s.ptr = nil; s.len = 0; s.cap = 0;\n"
|
|
" append(s, 10u8, 20u8, 30u8, 40u8);\n"
|
|
" let total: i32 = 0;\n"
|
|
" for (let b .. s) { total += b: i32; };\n"
|
|
" return total;\n"
|
|
"};", 100 },
|
|
/* alloc([], n): fresh empty slice with cap n. `package main;` for
|
|
* the same reason as the value-form test above (task #23 gate).
|
|
* Task #30 graduated the slice form to `([]T | nomem)`; the `!`
|
|
* aborts on OOM. */
|
|
{ "package main;\n"
|
|
"import rt;\n"
|
|
"fn main() i32 = {\n"
|
|
" let s: []u8 = alloc([], 16)!;\n"
|
|
" append(s, 72u8, 105u8);\n"
|
|
" return s.cap;\n"
|
|
"};", 16 },
|
|
/* #45: alloc([], n) now defers element type to the let-init LHS.
|
|
* `[]rune` is 4B-per-element; the cgen shortcut scales count by
|
|
* size(T). Returns s.cap = 8. */
|
|
{ "package main;\n"
|
|
"import rt;\n"
|
|
"fn main() i32 = {\n"
|
|
" let s: []rune = alloc([], 8)!;\n"
|
|
" return s.cap;\n"
|
|
"};", 8 },
|
|
/* #45: same as above for `[]str` (16B-per-element). */
|
|
{ "package main;\n"
|
|
"import rt;\n"
|
|
"fn main() i32 = {\n"
|
|
" let s: []str = alloc([], 4)!;\n"
|
|
" return s.cap;\n"
|
|
"};", 4 },
|
|
/* #45: `?` form. doit propagates nomem to its (i32 | nomem)
|
|
* return; the alloc-slice shortcut emits MOVQ $nomem_tag, AX +
|
|
* propret on null. doit returns 12 on success; main unwraps. */
|
|
{ "package main;\n"
|
|
"import rt;\n"
|
|
"fn doit() (i32 | nomem) = {\n"
|
|
" let s: []str = alloc([], 12)?;\n"
|
|
" return s.cap: i32;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" return doit()!;\n"
|
|
"};", 12 },
|
|
/* alloc-builtin shadow (task #23): a non-main package declares
|
|
* `fn alloc(n: i64) i64` and calls it bare. The same-module gate
|
|
* must suppress the builtin and dispatch to the user fn so the
|
|
* call returns n+100. Pre-gate this site lands in the typed-builtin
|
|
* path: arg is i64 → returns *i64 → init-type fails against the
|
|
* declared i64 (and would over-allocate against rt_malloc anyway).
|
|
* Inline multi-`package` mirrors driver-concatenated layout; the
|
|
* `import myos;` directive is silently skipped by locate_import
|
|
* (no external module by that name). */
|
|
{ "package myos;\n"
|
|
"fn alloc(n: i64) i64 = { return n + 100; };\n"
|
|
"fn run() i64 = { return alloc(7); };\n"
|
|
"package main;\n"
|
|
"import myos;\n"
|
|
"fn main() i32 = {\n"
|
|
" return myos.run(): i32;\n"
|
|
"};", 107 },
|
|
/* variadic spread: append(dst, src...) iterates src */
|
|
{ "import os;\n"
|
|
"fn main() i32 = {\n"
|
|
" let src: []u8;\n"
|
|
" src.ptr = nil; src.len = 0; src.cap = 0;\n"
|
|
" append(src, 65u8, 66u8, 67u8);\n"
|
|
" let dst: []u8;\n"
|
|
" dst.ptr = nil; dst.len = 0; dst.cap = 0;\n"
|
|
" append(dst, src...);\n"
|
|
" os.write(1, dst.ptr, dst.len: u64);\n"
|
|
" os.write(1, \"\\n\".ptr, 1u64);\n"
|
|
" return dst.len;\n"
|
|
"};", 3 },
|
|
/* Hare-style tuple destructure in let */
|
|
{ "fn divmod(a: i64, b: i64) (i64, i64) = { return a / b, a % b; };\n"
|
|
"fn main() i32 = {\n"
|
|
" let (q, r) = divmod(17, 5);\n"
|
|
" return (q + r): i32;\n"
|
|
"};", 5 },
|
|
/* Hare-style tuple destructure in for-range */
|
|
{ "fn main() i32 = {\n"
|
|
" let buf: [4]i64;\n"
|
|
" buf[0] = 1; buf[1] = 10; buf[2] = 2; buf[3] = 20;\n"
|
|
" let s: [](i64, i64);\n"
|
|
" s.ptr = buf.ptr: *(i64, i64);\n"
|
|
" s.len = 2; s.cap = 2;\n"
|
|
" let total: i64 = 0;\n"
|
|
" for (let (k, v) .. s) { total += k + v; };\n"
|
|
" return total: i32;\n"
|
|
"};", 33 },
|
|
/* Hare-style tuple positional access: t.0, t.1 */
|
|
{ "fn pair() (i64, i64) = { return 10, 32; };\n"
|
|
"fn main() i32 = {\n"
|
|
" let t: (i64, i64) = pair();\n"
|
|
" return (t.0 + t.1): i32;\n"
|
|
"};", 42 },
|
|
/* Hare-style abort/assert + free() builtin. `package main;` for
|
|
* the alloc-builtin gate (task #23). Task #30 graduated the
|
|
* builtin to a fallible signature; the `!` aborts on OOM. */
|
|
{ "package main;\n"
|
|
"import os;\n"
|
|
"import rt;\n"
|
|
"type point = struct { x: i64, y: i64 };\n"
|
|
"fn main() i32 = {\n"
|
|
" let p: *point = alloc(point { x = 7, y = 35 })!;\n"
|
|
" let r: i64 = p.x + p.y;\n"
|
|
" free(p);\n"
|
|
" assert(r == 42, \"sum mismatch\\n\");\n"
|
|
" return r: i32;\n"
|
|
"};", 42 },
|
|
/* Hare-style struct embedding: bare-name embed + anonymous
|
|
* nested struct, fields promoted to the outer scope. Both
|
|
* field-by-field assignment and flat struct-literal init must
|
|
* see promoted fields. */
|
|
{ "type point = struct { x: i32, y: i32 };\n"
|
|
"type vec = struct {\n"
|
|
" point,\n"
|
|
" struct { z: i32 },\n"
|
|
" w: i32,\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let v: vec = vec { x = 1, y = 2, z = 3, w = 36 };\n"
|
|
" return v.x + v.y + v.z + v.w;\n"
|
|
"};", 42 },
|
|
/* 3-field tuple destructure in for-range */
|
|
{ "fn main() i32 = {\n"
|
|
" let buf: [3]i64;\n"
|
|
" buf[0] = 5; buf[1] = 7; buf[2] = 30;\n"
|
|
" let s: [](i64, i64, i64);\n"
|
|
" s.ptr = buf.ptr: *(i64, i64, i64);\n"
|
|
" s.len = 1; s.cap = 1;\n"
|
|
" let total: i64 = 0;\n"
|
|
" for (let (a, b, c) .. s) { total += a + b + c; };\n"
|
|
" return total: i32;\n"
|
|
"};", 42 },
|
|
/* Hare-style tagged union + match */
|
|
{ "fn parse(n: i64) (i64 | i32) = {\n"
|
|
" if (n < 0) { return 1: i32; };\n"
|
|
" return n;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let r: (i64 | i32) = parse(40);\n"
|
|
" let s: i64 = 0;\n"
|
|
" match (r) {\n"
|
|
" case let v: i64 => s = v;\n"
|
|
" case let e: i32 => s = -1;\n"
|
|
" };\n"
|
|
" return (s + 2): i32;\n"
|
|
"};", 42 },
|
|
/* ? propagation up the stack */
|
|
{ "fn try1(n: i64) (i64 | i32) = {\n"
|
|
" if (n < 0) { return 99: i32; };\n"
|
|
" return n;\n"
|
|
"};\n"
|
|
"fn try2(n: i64) (i64 | i32) = {\n"
|
|
" let v: i64 = try1(n)?;\n"
|
|
" return v + 100;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let r: (i64 | i32) = try2(-1);\n"
|
|
" let s: i64 = 0;\n"
|
|
" match (r) {\n"
|
|
" case let v: i64 => s = v;\n"
|
|
" case let e: i32 => s = e: i64;\n"
|
|
" };\n"
|
|
" return s: i32;\n"
|
|
"};", 99 },
|
|
/* match cases written in reverse variant order — dispatch must
|
|
* use the variant tag, not the case position */
|
|
{ "fn parse(n: i64) (i64 | i32) = {\n"
|
|
" if (n < 0) { return 7: i32; };\n"
|
|
" return n;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let r: (i64 | i32) = parse(-1);\n"
|
|
" match (r) {\n"
|
|
" case let e: i32 => return e;\n"
|
|
" case let v: i64 => return (v + 1000): i32;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};", 7 },
|
|
/* let-init from a bare variant value: tag must be synthesised */
|
|
{ "fn main() i32 = {\n"
|
|
" let r: (i64 | i32) = 7: i32;\n"
|
|
" match (r) {\n"
|
|
" case let v: i64 => return 1;\n"
|
|
" case let e: i32 => return e;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};", 7 },
|
|
/* let-init from an untyped literal: variant inclusion must let
|
|
* the assignability check through, and the default variant wins */
|
|
{ "fn main() i32 = {\n"
|
|
" let r: (i64 | i32) = 5;\n"
|
|
" match (r) {\n"
|
|
" case let v: i64 => return v: i32;\n"
|
|
" case let e: i32 => return 99;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};", 5 },
|
|
/* assignment to a tagged-union local: same tag synthesis */
|
|
{ "fn main() i32 = {\n"
|
|
" let r: (i64 | i32) = 0;\n"
|
|
" r = 9: i32;\n"
|
|
" match (r) {\n"
|
|
" case let v: i64 => return 1;\n"
|
|
" case let e: i32 => return e;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};", 9 },
|
|
/* default arm `case =>` */
|
|
{ "fn parse(n: i64) (i64 | i32) = {\n"
|
|
" if (n < 0) { return 1: i32; };\n"
|
|
" return n;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let r: (i64 | i32) = parse(-1);\n"
|
|
" match (r) {\n"
|
|
" case let v: i64 => return 1;\n"
|
|
" case => return 7;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};", 7 },
|
|
/* `case T =>` without binding still dispatches by tag */
|
|
{ "fn parse(n: i64) (i64 | i32) = {\n"
|
|
" if (n < 0) { return 1: i32; };\n"
|
|
" return n;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let r: (i64 | i32) = parse(40);\n"
|
|
" match (r) {\n"
|
|
" case i32 => return 1;\n"
|
|
" case let v: i64 => return v: i32;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};", 40 },
|
|
/* named alias over `!(A | B)`: param spill must size to the
|
|
* flattened union (8B tag + 8B payload), not 8B-scalar. Pinned
|
|
* the cstage/wwstage divergence where wwstage's istaggedtype
|
|
* was alias-blind and the slot+8 read trailed into saved BP. */
|
|
{ "type invalid = !i32;\n"
|
|
"type overflow = !void;\n"
|
|
"type error = !(invalid | overflow);\n"
|
|
"fn errcode(e: error) i32 = {\n"
|
|
" match (e) {\n"
|
|
" case let v: invalid => return v: i32;\n"
|
|
" case let v: overflow => return 99;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let e: error = 7: invalid;\n"
|
|
" return errcode(e);\n"
|
|
"};", 7 },
|
|
/* str-typed variant payload: let-init with a string literal,
|
|
* match-binding loads ptr+len from the slot */
|
|
{ "fn main() i32 = {\n"
|
|
" let r: (i64 | str) = \"hello, world\";\n"
|
|
" match (r) {\n"
|
|
" case let n: i64 => return 1;\n"
|
|
" case let s: str => return s.len: i32;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};", 12 },
|
|
/* assigning a string into a tagged-union local */
|
|
{ "fn main() i32 = {\n"
|
|
" let r: (i64 | str) = 0;\n"
|
|
" r = \"abc\";\n"
|
|
" match (r) {\n"
|
|
" case let n: i64 => return 1;\n"
|
|
" case let s: str => return s.len: i32;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};", 3 },
|
|
/* fn returning (T | str) — wide return ABI */
|
|
{ "fn parse(n: i64) (i64 | str) = {\n"
|
|
" if (n < 0) { return \"negative number\"; };\n"
|
|
" return n;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let r: (i64 | str) = parse(-1);\n"
|
|
" match (r) {\n"
|
|
" case let n: i64 => return 1;\n"
|
|
" case let s: str => return s.len: i32;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};", 15 },
|
|
/* ? propagating a str-typed error all the way up */
|
|
{ "fn try1(n: i64) (i64 | str) = {\n"
|
|
" if (n < 0) { return \"fail\"; };\n"
|
|
" return n;\n"
|
|
"};\n"
|
|
"fn try2(n: i64) (i64 | str) = {\n"
|
|
" let v: i64 = try1(n)?;\n"
|
|
" return v + 100;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let r: (i64 | str) = try2(-1);\n"
|
|
" match (r) {\n"
|
|
" case let n: i64 => return n: i32;\n"
|
|
" case let s: str => return s.len: i32;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};", 4 },
|
|
/* ? with different success types but shared error: operand
|
|
* (i32 | str), enclosing (i64 | str). Success widens. */
|
|
{ "fn inner(b: bool) (i32 | str) = {\n"
|
|
" if (b) { return 7; };\n"
|
|
" return \"err\";\n"
|
|
"};\n"
|
|
"fn outer(b: bool) (i64 | str) = {\n"
|
|
" let v: i32 = inner(b)?;\n"
|
|
" return v: i64 + 100;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let r: (i64 | str) = outer(false);\n"
|
|
" match (r) {\n"
|
|
" case let n: i64 => return n: i32;\n"
|
|
" case let s: str => return s.len: i32;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};", 3 },
|
|
/* defer runs queued exprs in LIFO order, before the return
|
|
* expression is evaluated. Each call appends a decimal digit
|
|
* to acc via *&acc — return reads the post-defer state. */
|
|
{ "fn rec(p: *i32, c: i32) i32 = {\n"
|
|
" *p = *p * 10 + c;\n"
|
|
" return 0;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let acc: i32 = 0;\n"
|
|
" defer rec(&acc, 1);\n"
|
|
" defer rec(&acc, 2);\n"
|
|
" defer rec(&acc, 3);\n"
|
|
" return acc;\n"
|
|
"};", 65 }, /* 321 mod 256 */
|
|
/* defer also fires on an implicit fall-through return (void fn). */
|
|
{ "fn rec(p: *i32, c: i32) i32 = {\n"
|
|
" *p = *p * 10 + c;\n"
|
|
" return 0;\n"
|
|
"};\n"
|
|
"fn run(p: *i32) void = {\n"
|
|
" defer rec(p, 7);\n"
|
|
" defer rec(p, 8);\n"
|
|
" // no explicit return — implicit fall-through path\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let acc: i32 = 0;\n"
|
|
" run(&acc);\n"
|
|
" return acc;\n"
|
|
"};", 87 }, /* defers fire 8, 7 → 8 then 87 */
|
|
/* yield from match-as-expression: each arm yields a value;
|
|
* the match itself is bound to a let. */
|
|
{ "fn pick(b: bool) (i32 | str) = {\n"
|
|
" if (b) { return 7; };\n"
|
|
" return \"abc\";\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let r: (i32 | str) = pick(true);\n"
|
|
" let v: i32 = match (r) {\n"
|
|
" case let n: i32 => yield n + 1;\n"
|
|
" case let s: str => yield s.len: i32 + 100;\n"
|
|
" };\n"
|
|
" return v;\n"
|
|
"};", 8 },
|
|
{ "fn pick(b: bool) (i32 | str) = {\n"
|
|
" if (b) { return 7; };\n"
|
|
" return \"abc\";\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let r: (i32 | str) = pick(false);\n"
|
|
" let v: i32 = match (r) {\n"
|
|
" case let n: i32 => yield n + 1;\n"
|
|
" case let s: str => yield s.len: i32 + 100;\n"
|
|
" };\n"
|
|
" return v;\n"
|
|
"};", 103 },
|
|
/* Nullable pointer folding: `(*T | void)` is one 8-byte word
|
|
* where null = void variant. match/is/as/?/! all key off the
|
|
* pointer-vs-null discriminator instead of a separate tag. */
|
|
{ "fn lookup(p: *i32, b: bool) (*i32 | void) = {\n"
|
|
" if (b) { return p; };\n"
|
|
" return;\n"
|
|
"};\n"
|
|
"fn use_arg(r: (*i32 | void)) i32 = {\n"
|
|
" match (r) {\n"
|
|
" case let q: *i32 => return *q;\n"
|
|
" case void => return 99;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let x: i32 = 42;\n"
|
|
" let ok: i32 = use_arg(lookup(&x, true));\n"
|
|
" let no: i32 = use_arg(lookup(&x, false));\n"
|
|
" if (ok != 42) { return 1; };\n"
|
|
" if (no != 99) { return 2; };\n"
|
|
" return 7;\n"
|
|
"};", 7 },
|
|
/* Nullable with is/as: discriminator is ptr-vs-null. */
|
|
{ "fn lookup(p: *i32, b: bool) (*i32 | void) = {\n"
|
|
" if (b) { return p; };\n"
|
|
" return;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let x: i32 = 42;\n"
|
|
" let r1: (*i32 | void) = lookup(&x, true);\n"
|
|
" let r2: (*i32 | void) = lookup(&x, false);\n"
|
|
" let acc: i32 = 0;\n"
|
|
" if (r1 is *i32) { acc += 1; };\n"
|
|
" if (r2 is void) { acc += 2; };\n"
|
|
" let p: *i32 = r1 as *i32;\n"
|
|
" if (*p == 42) { acc += 4; };\n"
|
|
" return acc;\n"
|
|
"};", 7 },
|
|
/* `!`-flagged error variants: success picked by absence of `!`,
|
|
* errors picked by presence. Tag remap still works across
|
|
* different variant orders between operand and enclosing fn. */
|
|
{ "type invalid = !i32;\n"
|
|
"type overflow = !void;\n"
|
|
"fn inner(n: i32) (invalid | i64 | overflow) = {\n"
|
|
" if (n == 0) { return 7: invalid; };\n"
|
|
" if (n < 0) { return void: overflow; };\n"
|
|
" return n: i64 + 1000;\n"
|
|
"};\n"
|
|
"fn outer(n: i32) (overflow | i64 | invalid) = {\n"
|
|
" let v: i64 = inner(n)?;\n"
|
|
" return v + 1;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let r: (overflow | i64 | invalid) = outer(0);\n"
|
|
" match (r) {\n"
|
|
" case let v: i64 => return v: i32;\n"
|
|
" case let e: invalid => return e + 100;\n"
|
|
" case let e: overflow => return 999;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};", 107 },
|
|
/* ? with reversed error variant order: operand (i32 | str | bool),
|
|
* enclosing (i64 | bool | str). Tag remap maps str:1→2, bool:2→1. */
|
|
{ "fn inner(n: i32) (i32 | str | bool) = {\n"
|
|
" if (n == 0) { return \"z\"; };\n"
|
|
" if (n < 0) { return false; };\n"
|
|
" return n;\n"
|
|
"};\n"
|
|
"fn outer(n: i32) (i64 | bool | str) = {\n"
|
|
" let v: i32 = inner(n)?;\n"
|
|
" return v: i64 + 1000;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let r: (i64 | bool | str) = outer(-1);\n"
|
|
" match (r) {\n"
|
|
" case let n: i64 => return n: i32;\n"
|
|
" case let b: bool => { if (!b) { return 7; }; return 8; };\n"
|
|
" case let s: str => return 9;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};", 7 },
|
|
/* ! success unwrap when the first variant is itself str.
|
|
* Uses ! (not ?) because main's return is i32, not tagged —
|
|
* ? would require error variants to be propagatable to the
|
|
* enclosing return. ! aborts on the error variant instead. */
|
|
{ "fn make() (str | i64) = {\n"
|
|
" return \"ok\";\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let r: (str | i64) = make();\n"
|
|
" let v: str = r!;\n"
|
|
" return v.len: i32;\n"
|
|
"};", 2 },
|
|
/* type error = str; named-alias variant works through the
|
|
* full happy/error path */
|
|
{ "type error = str;\n"
|
|
"fn read(n: i64) (i64 | error) = {\n"
|
|
" if (n < 0) { return \"eof\": error; };\n"
|
|
" return n + 1;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let r: (i64 | error) = read(-1);\n"
|
|
" match (r) {\n"
|
|
" case let v: i64 => return v: i32;\n"
|
|
" case let e: error => return e.len: i32;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};", 3 },
|
|
/* multi-pattern arm: `case T1 | T2 =>` matches either tag */
|
|
{ "fn pick(n: i64) (i64 | i32 | u32) = {\n"
|
|
" if (n < 0) { return 1: i32; };\n"
|
|
" if (n == 0) { return 2: u32; };\n"
|
|
" return n;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let r1: (i64 | i32 | u32) = pick(0);\n"
|
|
" let r2: (i64 | i32 | u32) = pick(-1);\n"
|
|
" let r3: (i64 | i32 | u32) = pick(7);\n"
|
|
" let acc: i32 = 0;\n"
|
|
" match (r1) {\n"
|
|
" case let v: i64 => acc += 100;\n"
|
|
" case i32 | u32 => acc += 1;\n"
|
|
" };\n"
|
|
" match (r2) {\n"
|
|
" case let v: i64 => acc += 100;\n"
|
|
" case i32 | u32 => acc += 10;\n"
|
|
" };\n"
|
|
" match (r3) {\n"
|
|
" case let v: i64 => acc += v: i32;\n"
|
|
" case i32 | u32 => acc += 100;\n"
|
|
" };\n"
|
|
" return acc;\n"
|
|
"};", 18 },
|
|
/* Named-alias tagged union as fn arg + ≤16B variants */
|
|
{ "type result = (i64 | i32);\n"
|
|
"fn unwrap(r: result) i64 = {\n"
|
|
" match (r) {\n"
|
|
" case let v: i64 => return v;\n"
|
|
" case let e: i32 => return e: i64;\n"
|
|
" };\n"
|
|
" return -1;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let r1: result = 100;\n"
|
|
" let r2: result = 7: i32;\n"
|
|
" return (unwrap(r1) + unwrap(r2)): i32;\n"
|
|
"};", 107 },
|
|
/* 24B tagged-union arg with str variant */
|
|
{ "type result = (i64 | str);\n"
|
|
"fn classify(r: result) i32 = {\n"
|
|
" match (r) {\n"
|
|
" case let v: i64 => return 1;\n"
|
|
" case let e: str => return e.len: i32;\n"
|
|
" };\n"
|
|
" return -1;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let r1: result = \"hello\";\n"
|
|
" let r2: result = 42;\n"
|
|
" return classify(r1) + classify(r2);\n"
|
|
"};", 6 },
|
|
/* Tagged union as struct field — both literal init and assign,
|
|
* and match-on-field reads from the field's slot in place */
|
|
{ "type point = struct {\n"
|
|
" x: i32,\n"
|
|
" err: (i64 | str),\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let p: point = point { x = 1, err = 0 };\n"
|
|
" p.err = \"updated\";\n"
|
|
" match (p.err) {\n"
|
|
" case let v: i64 => return 0;\n"
|
|
" case let e: str => return e.len: i32;\n"
|
|
" };\n"
|
|
" return -1;\n"
|
|
"};", 7 },
|
|
/* Pointer variant in a tagged union */
|
|
{ "type point = struct { x: i32, y: i32 };\n"
|
|
"fn main() i32 = {\n"
|
|
" let p: point = point { x = 3, y = 4 };\n"
|
|
" let r: (*point | str) = &p;\n"
|
|
" match (r) {\n"
|
|
" case let pp: *point => return pp.x + pp.y;\n"
|
|
" case let e: str => return -1;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};", 7 },
|
|
/* Forwarding `return inner(n)` when both fns share a tagged-
|
|
* union return type — value passes through unwrapped */
|
|
{ "type result = (i64 | str);\n"
|
|
"fn inner(n: i64) result = {\n"
|
|
" if (n < 0) { return \"neg\"; };\n"
|
|
" return n + 1;\n"
|
|
"};\n"
|
|
"fn outer(n: i64) result = {\n"
|
|
" return inner(n);\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let r: result = outer(-1);\n"
|
|
" match (r) {\n"
|
|
" case let v: i64 => return v: i32;\n"
|
|
" case let e: str => return e.len: i32;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};", 3 },
|
|
/* match directly on a call expression (no intermediate let) */
|
|
{ "fn make(n: i64) (i64 | str) = {\n"
|
|
" if (n < 0) { return \"neg\"; };\n"
|
|
" return n + 1;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" match (make(-1)) {\n"
|
|
" case let v: i64 => return v: i32;\n"
|
|
" case let e: str => return e.len: i32;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};", 3 },
|
|
/* Struct variant of a tagged union at the call site. The arg is
|
|
* an N_STRUCTLIT, the param is (str|point). Widening at the call
|
|
* site must zero-fill the scratch slot, store each field at
|
|
* slot+8+field_off, then push the slot words high→low. */
|
|
{ "type point = struct { x: i32, y: i32 };\n"
|
|
"fn classify(r: (str | point)) i32 = {\n"
|
|
" match (r) {\n"
|
|
" case let s: str => return 0 - s.len: i32;\n"
|
|
" case let p: point => return p.x + p.y;\n"
|
|
" };\n"
|
|
" return -1;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" return classify(point { x = 10, y = 20 });\n"
|
|
"};", 30 },
|
|
/* Struct variant passed as a typed local. Widening copies the
|
|
* struct words from the local into the scratch slot at +8. */
|
|
{ "type point = struct { x: i32, y: i32 };\n"
|
|
"fn classify(r: (str | point)) i32 = {\n"
|
|
" match (r) {\n"
|
|
" case let s: str => return 0 - s.len: i32;\n"
|
|
" case let p: point => return p.x + p.y;\n"
|
|
" };\n"
|
|
" return -1;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let p: point = point { x = 11, y = 22 };\n"
|
|
" return classify(p);\n"
|
|
"};", 33 },
|
|
/* let-init of a tagged-union local from a struct literal: the
|
|
* field stores go into slot+8+field_off in-place; tag patched
|
|
* last. */
|
|
{ "type point = struct { x: i32, y: i32 };\n"
|
|
"fn main() i32 = {\n"
|
|
" let r: (str | point) = point { x = 7, y = 35 };\n"
|
|
" match (r) {\n"
|
|
" case let s: str => return 0 - s.len: i32;\n"
|
|
" case let p: point => return p.x + p.y;\n"
|
|
" };\n"
|
|
" return -1;\n"
|
|
"};", 42 },
|
|
/* Reassign a tagged-union local to a struct literal. Same path
|
|
* as let-init but writing into an already-allocated slot. */
|
|
{ "type point = struct { x: i32, y: i32 };\n"
|
|
"fn main() i32 = {\n"
|
|
" let r: (str | point) = \"init\";\n"
|
|
" r = point { x = 100, y = 23 };\n"
|
|
" match (r) {\n"
|
|
" case let s: str => return 0 - s.len: i32;\n"
|
|
" case let p: point => return p.x + p.y;\n"
|
|
" };\n"
|
|
" return -1;\n"
|
|
"};", 123 },
|
|
/* Return a struct variant of the fn's tagged return type. The
|
|
* scratch-slot path materialises the struct payload then loads
|
|
* AX/DX/CX from it. */
|
|
{ "type point = struct { x: i32, y: i32 };\n"
|
|
"fn make() (str | point) = {\n"
|
|
" return point { x = 12, y = 30 };\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let r: (str | point) = make();\n"
|
|
" match (r) {\n"
|
|
" case let s: str => return 0 - s.len: i32;\n"
|
|
" case let p: point => return p.x + p.y;\n"
|
|
" };\n"
|
|
" return -1;\n"
|
|
"};", 42 },
|
|
/* Widen a smaller tagged union to a wider one across slot sizes
|
|
* AND remapped variant indices. (i32 | rune) is 16B with i32 at
|
|
* tag 0; (str | i32 | rune) is 24B with i32 at tag 1. The widen
|
|
* path copies the slot words, zero-pads to 24B, then runs a
|
|
* CMPQ-chain switch to remap src tag 0 → dst tag 1. */
|
|
{ "fn classify(r: (str | i32 | rune)) i32 = {\n"
|
|
" match (r) {\n"
|
|
" case let s: str => return 1;\n"
|
|
" case let n: i32 => return n;\n"
|
|
" case let c: rune => return c: i32 + 100;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let inner: (i32 | rune) = 42: i32;\n"
|
|
" return classify(inner);\n"
|
|
"};", 42 },
|
|
/* Same shape but the rune variant of inner exercises the tag
|
|
* remap from src tag 1 → dst tag 2. The rune literal needs an
|
|
* explicit `: rune` cast — `'A'` is an untyped rune and the
|
|
* variant search picks the first variant that accepts it (i32,
|
|
* which also accepts untyped runes). 'A' = 65 + 100 = 165. */
|
|
{ "fn classify(r: (str | i32 | rune)) i32 = {\n"
|
|
" match (r) {\n"
|
|
" case let s: str => return 1;\n"
|
|
" case let n: i32 => return n;\n"
|
|
" case let c: rune => return c: i32 + 100;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let inner: (i32 | rune) = 'A': rune;\n"
|
|
" return classify(inner);\n"
|
|
"};", 165 },
|
|
/* let-init of a wider tagged union from a smaller-tagged local. */
|
|
{ "fn main() i32 = {\n"
|
|
" let inner: (i32 | rune) = 42: i32;\n"
|
|
" let r: (str | i32 | rune) = inner;\n"
|
|
" match (r) {\n"
|
|
" case let s: str => return 1;\n"
|
|
" case let n: i32 => return n;\n"
|
|
" case let c: rune => return c: i32 + 100;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};", 42 },
|
|
/* Spread variant in tagged-union type — `(...inner | T)`. The
|
|
* checker flattens the spread's variants into the enclosing
|
|
* union so `outer` has variants {i32, rune, str}. */
|
|
{ "type inner = (i32 | rune);\n"
|
|
"type outer = (...inner | str);\n"
|
|
"fn main() i32 = {\n"
|
|
" let r: outer = 42: i32;\n"
|
|
" match (r) {\n"
|
|
" case let n: i32 => return n;\n"
|
|
" case let c: rune => return c: i32;\n"
|
|
" case let s: str => return 0;\n"
|
|
" };\n"
|
|
" return -1;\n"
|
|
"};", 42 },
|
|
/* Tagged-union element in a fixed array — scalar+str variants.
|
|
* Store via N_INDEX widening, read+match through cgexpr N_INDEX
|
|
* tagged-slot load. 10 + len("hi")=2 + 5 = 17. */
|
|
{ "fn main() i32 = {\n"
|
|
" let arr: [3](i32 | str);\n"
|
|
" arr[0] = 10;\n"
|
|
" arr[1] = \"hi\";\n"
|
|
" arr[2] = 5;\n"
|
|
" let s: i32 = 0;\n"
|
|
" let i: i32 = 0;\n"
|
|
" for (i < 3) {\n"
|
|
" match (arr[i]) {\n"
|
|
" case let v: i32 => s += v;\n"
|
|
" case let t: str => s += t.len: i32;\n"
|
|
" };\n"
|
|
" i += 1;\n"
|
|
" };\n"
|
|
" return s;\n"
|
|
"};", 17 },
|
|
/* Tagged-union array with struct payload variant. Struct fields
|
|
* are written at slot+8+field_off via cg_widen_tagged_store; the
|
|
* read side just copies slot bytes into AX/DX/CX for match.
|
|
* 5 + (7+11) + 2 + 9 = 34. */
|
|
{ "type pair = struct { a: i32, b: i32 };\n"
|
|
"fn main() i32 = {\n"
|
|
" let arr: [4](i32 | pair | str);\n"
|
|
" arr[0] = 5;\n"
|
|
" arr[1] = pair { a = 7, b = 11 };\n"
|
|
" arr[2] = \"yo\";\n"
|
|
" arr[3] = 9;\n"
|
|
" let s: i32 = 0;\n"
|
|
" let i: i32 = 0;\n"
|
|
" for (i < 4) {\n"
|
|
" match (arr[i]) {\n"
|
|
" case let v: i32 => s += v;\n"
|
|
" case let p: pair => s += p.a + p.b;\n"
|
|
" case let t: str => s += t.len: i32;\n"
|
|
" };\n"
|
|
" i += 1;\n"
|
|
" };\n"
|
|
" return s;\n"
|
|
"};", 34 },
|
|
/* Slicing an array of tagged elements — the slice load path
|
|
* uses the fallback (non-ident base) N_INDEX which loads slot
|
|
* bytes from a computed address. 1 + 2 + 3 + 4 = 10. */
|
|
{ "fn main() i32 = {\n"
|
|
" let buf: [4](i32 | str);\n"
|
|
" buf[0] = 1;\n"
|
|
" buf[1] = \"ww\";\n"
|
|
" buf[2] = 3;\n"
|
|
" buf[3] = 4;\n"
|
|
" let xs: [](i32 | str) = buf[0:4];\n"
|
|
" let s: i32 = 0;\n"
|
|
" let i: i32 = 0;\n"
|
|
" for (i < 4) {\n"
|
|
" match (xs[i]) {\n"
|
|
" case let v: i32 => s += v;\n"
|
|
" case let t: str => s += t.len: i32;\n"
|
|
" };\n"
|
|
" i += 1;\n"
|
|
" };\n"
|
|
" return s;\n"
|
|
"};", 10 },
|
|
/* Passing arr[i] (tagged element) as a tagged arg — cgexpr leaves
|
|
* the slot in AX/DX/CX which the call-site shuffle pushes onto
|
|
* the arg stack. 5 + 30 (len 3 * 10) + 7 = 42. */
|
|
{ "fn weight(v: (i32 | str)) i32 = {\n"
|
|
" match (v) {\n"
|
|
" case let n: i32 => return n;\n"
|
|
" case let s: str => return s.len: i32 * 10;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let arr: [3](i32 | str);\n"
|
|
" arr[0] = 5;\n"
|
|
" arr[1] = \"abc\";\n"
|
|
" arr[2] = 7;\n"
|
|
" let s: i32 = 0;\n"
|
|
" let i: i32 = 0;\n"
|
|
" for (i < 3) {\n"
|
|
" s += weight(arr[i]);\n"
|
|
" i += 1;\n"
|
|
" };\n"
|
|
" return s;\n"
|
|
"};", 42 },
|
|
/* let-init of a tagged local from arr[i] — the let path routes
|
|
* through cg_widen_tagged_store which, for a tagged source via
|
|
* cgexpr, spills AX/DX/CX into the slot. "ww!".len == 3. */
|
|
{ "fn main() i32 = {\n"
|
|
" let arr: [3](i32 | str);\n"
|
|
" arr[0] = 11;\n"
|
|
" arr[1] = \"ww!\";\n"
|
|
" arr[2] = 7;\n"
|
|
" let r: (i32 | str) = arr[1];\n"
|
|
" match (r) {\n"
|
|
" case let n: i32 => return n;\n"
|
|
" case let s: str => return s.len: i32;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};", 3 },
|
|
/* Tagged-subset store into a wider tagged-union array element:
|
|
* source slot is (i32|str), dest element is (i32|str|u64). The
|
|
* store path materialises the subset in scratch then copies slot
|
|
* bytes — tag remap is a no-op here (variant order matches).
|
|
* arr[0]=9:i32, arr[1]="hi":str → 9 + 2 = 11. */
|
|
{ "type inner = (i32 | str);\n"
|
|
"fn main() i32 = {\n"
|
|
" let arr: [2](i32 | str | u64);\n"
|
|
" let v: inner = 9;\n"
|
|
" arr[0] = v;\n"
|
|
" let w: inner = \"hi\";\n"
|
|
" arr[1] = w;\n"
|
|
" let s: i32 = 0;\n"
|
|
" let i: i32 = 0;\n"
|
|
" for (i < 2) {\n"
|
|
" match (arr[i]) {\n"
|
|
" case let n: i32 => s += n;\n"
|
|
" case let t: str => s += t.len: i32;\n"
|
|
" case let u: u64 => s += 100;\n"
|
|
" };\n"
|
|
" i += 1;\n"
|
|
" };\n"
|
|
" return s;\n"
|
|
"};", 11 },
|
|
/* Returning arr[i] from a fn whose return type matches the
|
|
* element. cgexpr leaves slot in AX/DX/CX; the return path
|
|
* forwards as-is. arr[1] = "abc" → str variant → .len == 3. */
|
|
{ "fn pick(i: i32) (i32 | str) = {\n"
|
|
" let arr: [2](i32 | str);\n"
|
|
" arr[0] = 21;\n"
|
|
" arr[1] = \"abc\";\n"
|
|
" return arr[i];\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let r: (i32 | str) = pick(1);\n"
|
|
" match (r) {\n"
|
|
" case let n: i32 => return n;\n"
|
|
" case let s: str => return s.len: i32;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};", 3 },
|
|
/* Nullable folded element `(*T | void)` — the slot is one 8B
|
|
* pointer word; null is the void variant. Stores route through
|
|
* the nullable branch of cg_widen_tagged_store (single MOVQ at
|
|
* +0). 100 (nil) + 1 (non-nil) + 100 (nil) = 201. */
|
|
{ "fn pickptr(b: bool) *i32 = {\n"
|
|
" let x: i32 = 42;\n"
|
|
" if (b) { return &x; };\n"
|
|
" return nil;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let arr: [3](*i32 | void);\n"
|
|
" arr[0] = nil;\n"
|
|
" arr[1] = pickptr(true);\n"
|
|
" arr[2] = pickptr(false);\n"
|
|
" let s: i32 = 0;\n"
|
|
" let i: i32 = 0;\n"
|
|
" for (i < 3) {\n"
|
|
" match (arr[i]) {\n"
|
|
" case let p: *i32 => s += 1;\n"
|
|
" case => s += 100;\n"
|
|
" };\n"
|
|
" i += 1;\n"
|
|
" };\n"
|
|
" return s;\n"
|
|
"};", 201 },
|
|
/* Plan 9-style sentinel error idiom: `def NAME: error = "lit"`
|
|
* inlines as the (ptr, len) pair at use sites. */
|
|
{ "type error = str;\n"
|
|
"def eEOF: error = \"eof\";\n"
|
|
"def eShortRead: error = \"short read\";\n"
|
|
"fn read(n: i64) (i64 | error) = {\n"
|
|
" if (n < 0) { return eEOF; };\n"
|
|
" if (n == 0) { return eShortRead; };\n"
|
|
" return n + 1;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let r0: (i64 | error) = read(0);\n"
|
|
" let r1: (i64 | error) = read(-1);\n"
|
|
" let r2: (i64 | error) = read(5);\n"
|
|
" let acc: i32 = 0;\n"
|
|
" match (r0) {\n"
|
|
" case let v: i64 => acc += 100;\n"
|
|
" case let e: error => acc += e.len: i32;\n"
|
|
" };\n"
|
|
" match (r1) {\n"
|
|
" case let v: i64 => acc += 100;\n"
|
|
" case let e: error => acc += e.len: i32;\n"
|
|
" };\n"
|
|
" match (r2) {\n"
|
|
" case let v: i64 => acc += v: i32;\n"
|
|
" case let e: error => acc += 100;\n"
|
|
" };\n"
|
|
" return acc;\n"
|
|
"};", 19 },
|
|
/* End-to-end stdlib usage: pull in lib/os and exercise the
|
|
* fallible API tryread/trywrite returning (i64 | oserror) over
|
|
* a real syscall. oserror carries -errno; on a bad fd we expect
|
|
* -EBADF (-9). */
|
|
{ "import os;\n"
|
|
"fn main() i32 = {\n"
|
|
" let buf: [3]u8;\n"
|
|
" buf[0] = 88: u8;\n"
|
|
" let ok: (i64 | os.oserror) = os.trywrite(1, buf.ptr, 1u64);\n"
|
|
" let bad: (i64 | os.oserror) = os.trywrite(999: i32, buf.ptr, 1u64);\n"
|
|
" let acc: i32 = 0;\n"
|
|
" match (ok) {\n"
|
|
" case let n: i64 => acc += n: i32;\n"
|
|
" case let e: os.oserror => acc += -100;\n"
|
|
" };\n"
|
|
" match (bad) {\n"
|
|
" case let n: i64 => acc += -100;\n"
|
|
" case let e: os.oserror => acc += (- (e: i64)): i32;\n"
|
|
" };\n"
|
|
" return acc;\n"
|
|
"};", 10 }, /* 1 byte written + 9 (EBADF) */
|
|
/* strconv.stoi64: fallible signed decimal, graduated to
|
|
* (i64 | invalid | overflow). invalid carries the offending
|
|
* index; overflow is the void variant. */
|
|
{ "import strconv;\n"
|
|
"type r_t = (i64 | strconv.invalid | strconv.overflow);\n"
|
|
"fn main() i32 = {\n"
|
|
" let r1: r_t = strconv.stoi64(\"42\", strconv.base.DEC);\n"
|
|
" let r2: r_t = strconv.stoi64(\"-7\", strconv.base.DEC);\n"
|
|
" let r3: r_t = strconv.stoi64(\"abc\", strconv.base.DEC);\n"
|
|
" let acc: i32 = 0;\n"
|
|
" match (r1) {\n"
|
|
" case let v: i64 => acc += v: i32;\n"
|
|
" case let e: strconv.invalid => acc += -100;\n"
|
|
" case let e: strconv.overflow => acc += -200;\n"
|
|
" };\n"
|
|
" match (r2) {\n"
|
|
" case let v: i64 => acc += v: i32;\n"
|
|
" case let e: strconv.invalid => acc += -100;\n"
|
|
" case let e: strconv.overflow => acc += -200;\n"
|
|
" };\n"
|
|
" match (r3) {\n"
|
|
" case let v: i64 => acc += -100;\n"
|
|
" case let e: strconv.invalid => acc += e: i32;\n"
|
|
" case let e: strconv.overflow => acc += -200;\n"
|
|
" };\n"
|
|
" return acc;\n"
|
|
"};", 35 }, /* 42 + (-7) + 0 (invalid at index 0 in \"abc\") */
|
|
/* strconv.stou64: success path; a leading '-' is rejected with
|
|
* overflow per Hare (ref/hare/strconv/stou.ha:72-74 — parseint
|
|
* accepts the sign, stou64 then rejects sign==true as overflow).
|
|
* Updated from the prior ad-hoc parse, which mis-reported it as
|
|
* invalid(index 0) (strconv-int fold-1 fidelity fix). */
|
|
{ "import strconv;\n"
|
|
"type r_t = (u64 | strconv.invalid | strconv.overflow);\n"
|
|
"fn main() i32 = {\n"
|
|
" let r1: r_t = strconv.stou64(\"123\", strconv.base.DEC);\n"
|
|
" let r2: r_t = strconv.stou64(\"-1\", strconv.base.DEC);\n"
|
|
" let acc: i32 = 0;\n"
|
|
" match (r1) {\n"
|
|
" case let v: u64 => acc += v: i32;\n"
|
|
" case let e: strconv.invalid => acc += -100;\n"
|
|
" case let e: strconv.overflow => acc += -200;\n"
|
|
" };\n"
|
|
" match (r2) {\n"
|
|
" case let v: u64 => acc += 100;\n"
|
|
" case let e: strconv.invalid => acc += 200;\n"
|
|
" case let e: strconv.overflow => acc += 7;\n"
|
|
" };\n"
|
|
" return acc;\n"
|
|
"};", 130 }, /* 123 + 7: \"-1\" is overflow (not invalid/success) */
|
|
/* strings.byteindex with (str | rune) needle: returns (i32 | void). */
|
|
{ "import strings;\n"
|
|
"fn pick(r: (i32 | void), miss: i32) i32 = {\n"
|
|
" match (r) {\n"
|
|
" case let i: i32 => return i;\n"
|
|
" case void => return miss;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let s: str = \"hello, world\";\n"
|
|
" let i1: i32 = pick(strings.byteindex(s, ','), -1);\n"
|
|
" let i2: i32 = pick(strings.byteindex(s, 'z'), -1);\n"
|
|
" let i3: i32 = pick(strings.byteindex(s, \"world\"), -1);\n"
|
|
" let i4: i32 = pick(strings.byteindex(s, \"nope\"), -1);\n"
|
|
" return i1 + i2 + i3 + i4;\n"
|
|
"};", 10 }, /* 5 + (-1) + 7 + (-1) */
|
|
/* bytes.index: substring search over []u8, (i32 | void). */
|
|
{ "import bytes;\n"
|
|
"fn main() i32 = {\n"
|
|
" let buf: [12]u8;\n"
|
|
" buf[0] = 104u8; buf[1] = 101u8; buf[2] = 108u8; buf[3] = 108u8;\n"
|
|
" buf[4] = 111u8; buf[5] = 44u8; buf[6] = 32u8; buf[7] = 119u8;\n"
|
|
" buf[8] = 111u8; buf[9] = 114u8; buf[10] = 108u8; buf[11] = 100u8;\n"
|
|
" let needle: [3]u8;\n"
|
|
" needle[0] = 119u8; needle[1] = 111u8; needle[2] = 114u8;\n"
|
|
" let r: (i32 | void) = bytes.index(buf[0:12], needle[0:3]);\n"
|
|
" match (r) {\n"
|
|
" case let i: i32 => return i;\n"
|
|
" case void => return -1;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};", 7 },
|
|
/* errors named-void tags — dispatch through a (T | tag | tag)
|
|
* union, one variant per error condition. Replaces the old
|
|
* errors.equal sentinel-string comparison. */
|
|
{ "import errors;\n"
|
|
"fn parse(n: i64) (i64 | errors.invalid | errors.noentry) = {\n"
|
|
" if (n < 0) { let e: errors.invalid; return e; };\n"
|
|
" if (n == 0) { let e: errors.noentry; return e; };\n"
|
|
" return n;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let r1: (i64 | errors.invalid | errors.noentry) = parse(-1);\n"
|
|
" let r2: (i64 | errors.invalid | errors.noentry) = parse(0);\n"
|
|
" let acc: i32 = 0;\n"
|
|
" match (r1) {\n"
|
|
" case let v: i64 => acc += -100;\n"
|
|
" case let e: errors.invalid => acc += 1;\n"
|
|
" case let e: errors.noentry => acc += -100;\n"
|
|
" };\n"
|
|
" match (r2) {\n"
|
|
" case let v: i64 => acc += -100;\n"
|
|
" case let e: errors.invalid => acc += -100;\n"
|
|
" case let e: errors.noentry => acc += 10;\n"
|
|
" };\n"
|
|
" return acc;\n"
|
|
"};", 11 },
|
|
/* errors remaining named-void tags — pin the rest of the surface
|
|
* (noaccess / exists / unsupported) so each is callable as both a
|
|
* return variant and a match arm. One row per tag would bloat the
|
|
* table; fold them into one (T | A | B | C) dispatch. */
|
|
{ "import errors;\n"
|
|
"fn classify(n: i32) (i32 | errors.noaccess | errors.exists | errors.unsupported) = {\n"
|
|
" if (n == 1) { let e: errors.noaccess; return e; };\n"
|
|
" if (n == 2) { let e: errors.exists; return e; };\n"
|
|
" if (n == 3) { let e: errors.unsupported; return e; };\n"
|
|
" return n;\n"
|
|
"};\n"
|
|
"fn dispatch(r: (i32 | errors.noaccess | errors.exists | errors.unsupported)) i32 = {\n"
|
|
" match (r) {\n"
|
|
" case let v: i32 => return v;\n"
|
|
" case let e: errors.noaccess => return 10;\n"
|
|
" case let e: errors.exists => return 20;\n"
|
|
" case let e: errors.unsupported => return 30;\n"
|
|
" };\n"
|
|
" return -1;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" return dispatch(classify(1)) + dispatch(classify(2)) + dispatch(classify(3));\n"
|
|
"};", 60 },
|
|
/* bufio.scanline: drain '\\n'-terminated lines from a memio.fixed
|
|
* source through a bufio.scanner. The trailing "baz" fragment has
|
|
* no newline; under the EOF_DISCARD default it's dropped and the
|
|
* call returns io.eof. Also pins the cross-module type ref
|
|
* (scanner stores *io.stream) end-to-end through `use`. */
|
|
{ "import bufio;\n"
|
|
"import io;\n"
|
|
"import memio;\n"
|
|
"fn main() i32 = {\n"
|
|
" let raw: [11]u8;\n"
|
|
" raw[0] = 102u8; raw[1] = 111u8; raw[2] = 111u8; raw[3] = 10u8;\n"
|
|
" raw[4] = 98u8; raw[5] = 97u8; raw[6] = 114u8; raw[7] = 10u8;\n"
|
|
" raw[8] = 98u8; raw[9] = 97u8; raw[10] = 122u8;\n"
|
|
" let mst: memio.stream = memio.fixed(raw[0:11]);\n"
|
|
" let m: io.stream = &mst.vt;\n"
|
|
" let buf: [16]u8;\n"
|
|
" let sc: bufio.scanner = bufio.newscannerbuf(m, buf[0:16]);\n"
|
|
" let acc: i32 = 0;\n"
|
|
" let l1: (str | io.eof | io.error | bufio.overflow) = bufio.scanline(&sc);\n"
|
|
" match (l1) {\n"
|
|
" case let s: str => acc += s.len;\n"
|
|
" case io.eof => acc += -100;\n"
|
|
" case let _e: io.error => acc += -1000;\n"
|
|
" case bufio.overflow => acc += -10000;\n"
|
|
" };\n"
|
|
" let l2: (str | io.eof | io.error | bufio.overflow) = bufio.scanline(&sc);\n"
|
|
" match (l2) {\n"
|
|
" case let s: str => acc += s.len;\n"
|
|
" case io.eof => acc += -100;\n"
|
|
" case let _e: io.error => acc += -1000;\n"
|
|
" case bufio.overflow => acc += -10000;\n"
|
|
" };\n"
|
|
" let l3: (str | io.eof | io.error | bufio.overflow) = bufio.scanline(&sc);\n"
|
|
" match (l3) {\n"
|
|
" case let s: str => acc += -100;\n"
|
|
" case io.eof => acc += 7;\n"
|
|
" case let _e: io.error => acc += -1000;\n"
|
|
" case bufio.overflow => acc += -10000;\n"
|
|
" };\n"
|
|
" return acc;\n"
|
|
"};", 13 }, /* 3 + 3 + 7 (eof arm) */
|
|
/* `(scalar, str)` tuple return: AX:DX:CX convention extends the
|
|
* tagged-union ABI. AX = scalar, DX = str.ptr, CX = str.len.
|
|
* Receive sites destructure off the same regs regardless of
|
|
* positional order. Without the fix, len was lost (only AX:DX
|
|
* returned), every receive shape gave garbage. */
|
|
{ "fn split() (i64, str) = { return 42, \"hello\"; };\n"
|
|
"fn main() i32 = {\n"
|
|
" let n, s = split();\n"
|
|
" return (n: i32) + (s.len: i32);\n"
|
|
"};", 47 },
|
|
{ "fn split() (str, i64) = { return \"hello\", 42; };\n"
|
|
"fn main() i32 = {\n"
|
|
" let s, n = split();\n"
|
|
" return (n: i32) + (s.len: i32);\n"
|
|
"};", 47 },
|
|
{ "fn split() (i64, str) = { return 42, \"hello\"; };\n"
|
|
"fn main() i32 = {\n"
|
|
" let t: (i64, str) = split();\n"
|
|
" return (t.0: i32) + (t.1.len: i32);\n"
|
|
"};", 47 },
|
|
/* Hare-style paren tuple-destructure with str element */
|
|
{ "fn split() (i64, str) = { return 42, \"hello\"; };\n"
|
|
"fn main() i32 = {\n"
|
|
" let (n, s) = split();\n"
|
|
" return (n: i32) + (s.len: i32);\n"
|
|
"};", 47 },
|
|
/* Chained field write through a pointer field: `r.sym.flag = v`
|
|
* where `r.sym: *T`. The cgen must evaluate the inner pointer,
|
|
* then store at *(ptr + field.offset). Without the fix, the
|
|
* single-level N_IDENT-base path doesn't fire (base is itself
|
|
* an N_DOT) and the assignment silently emits no instructions.
|
|
* Compound op + 1-byte field + 3-level chain all exercised. */
|
|
{ "type inner = struct { tag: u8, pad: u8, flag: i32 };\n"
|
|
"type outer = struct { sym: *inner };\n"
|
|
"fn main() i32 = {\n"
|
|
" let i: inner = inner { tag = 0u8, pad = 0u8, flag = 10 };\n"
|
|
" let r: outer = outer { sym = &i };\n"
|
|
" r.sym.flag += 32;\n"
|
|
" r.sym.tag = 5u8;\n"
|
|
" return r.sym.flag + (r.sym.tag: i32);\n"
|
|
"};", 47 },
|
|
{ "type leaf = struct { v: i32 };\n"
|
|
"type mid = struct { l: *leaf };\n"
|
|
"type top = struct { m: *mid };\n"
|
|
"fn main() i32 = {\n"
|
|
" let lf: leaf = leaf { v = 0 };\n"
|
|
" let md: mid = mid { l = &lf };\n"
|
|
" let tp: top = top { m = &md };\n"
|
|
" tp.m.l.v = 99;\n"
|
|
" return tp.m.l.v;\n"
|
|
"};", 99 },
|
|
/* `def NAME: str = \"lit\"` field access. The Sdef has no stack
|
|
* slot, so .len/.ptr must inline the literal length / strlit
|
|
* address; without the fix, .len reads BP+8 (return-address slot)
|
|
* as garbage. */
|
|
{ "def MSG: str = \"hello world\";\n"
|
|
"fn main() i32 = { return MSG.len: i32; };", 11 },
|
|
{ "import os;\n"
|
|
"def GREETING: str = \"hi\\n\";\n"
|
|
"fn main() i32 = {\n"
|
|
" os.write(1, GREETING.ptr, GREETING.len: u64);\n"
|
|
" return GREETING.len: i32;\n"
|
|
"};", 3 },
|
|
/* Hare-style `is` / `as`: type test returns bool, type assertion
|
|
* unwraps to the variant's value (success path only — abort path
|
|
* exit(1) is exercised manually). Covers i64/i32 scalar variants
|
|
* and str (16B variant via .len pseudo-field). */
|
|
{ "fn classify(n: i64) (i64 | i32) = {\n"
|
|
" if (n < 0) { return 7: i32; };\n"
|
|
" return n;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let ok: (i64 | i32) = classify(40);\n"
|
|
" let bad: (i64 | i32) = classify(-1);\n"
|
|
" let s: i32 = 0;\n"
|
|
" if (ok is i64) { s += 1; };\n"
|
|
" if (bad is i32) { s += 1; };\n"
|
|
" if (ok is i32) { s += 100; };\n"
|
|
" if (bad is i64) { s += 100; };\n"
|
|
" let v: i64 = ok as i64;\n"
|
|
" let e: i32 = bad as i32;\n"
|
|
" return (v: i32) + s + e;\n"
|
|
"};", 49 },
|
|
/* `as` on a 16B str variant — unwrap loads (ptr, len). */
|
|
{ "fn fail() (i64 | str) = { return \"bad\"; };\n"
|
|
"fn main() i32 = {\n"
|
|
" let r: (i64 | str) = fail();\n"
|
|
" let e: str = r as str;\n"
|
|
" return e.len: i32;\n"
|
|
"};", 3 },
|
|
/* enum: auto-increment, explicit value, sibling-ref */
|
|
{ "type color = enum { RED, GREEN, BLUE };\n"
|
|
"fn main() i32 = { return color.BLUE as i32; };", 2 },
|
|
{ "type mode = enum u8 { R = 1, W = 2, RW = R | W };\n"
|
|
"fn main() i32 = {\n"
|
|
" let m: mode = mode.RW;\n"
|
|
" return m as i32;\n"
|
|
"};", 3 },
|
|
/* enum: bitwise op between two members yields the same enum type */
|
|
{ "type mode = enum u8 { R = 1, W = 2 };\n"
|
|
"fn main() i32 = {\n"
|
|
" let m: mode = mode.R | mode.W;\n"
|
|
" return m as i32;\n"
|
|
"};", 3 },
|
|
/* enum: pkg-qualified access — `pkg.dir.SOUTH` resolves through
|
|
* SK_USE and folds to the member literal. */
|
|
{ "package pkg;\n"
|
|
"type dir = enum { NORTH, SOUTH, EAST, WEST };\n"
|
|
"package main;\n"
|
|
"import pkg;\n"
|
|
"fn main() i32 = { return pkg.dir.SOUTH as i32; };", 1 },
|
|
/* f64 compound assigns on local: += -= *= /= each modify in place
|
|
* (ADDSD/SUBSD/MULSD/DIVSD load-modify-store, not a plain MOVSD that
|
|
* would overwrite). 1.5 + 0.5 = 2.0 → 2.0 - 1.0 = 1.0 → 1.0 * 4.0 =
|
|
* 4.0 → 4.0 / 2.0 = 2.0 → return 2. */
|
|
{ "fn main() i32 = {\n"
|
|
" let a: f64 = 1.5;\n"
|
|
" a += 0.5;\n"
|
|
" a -= 1.0;\n"
|
|
" a *= 4.0;\n"
|
|
" a /= 2.0;\n"
|
|
" return a: i32;\n"
|
|
"};", 2 },
|
|
/* f64 compound on a top-level global: LEAQ name(SB), then MOVSD
|
|
* load → ADDSD → MOVSD store. 10.0 + 5.0 = 15.0 → 15.0 * 2.0 =
|
|
* 30.0 → 30.0 - 20.0 = 10.0 → 10.0 / 5.0 = 2.0. */
|
|
{ "let G: f64 = 10.0;\n"
|
|
"fn main() i32 = {\n"
|
|
" G += 5.0;\n"
|
|
" G *= 2.0;\n"
|
|
" G -= 20.0;\n"
|
|
" G /= 5.0;\n"
|
|
" return G: i32;\n"
|
|
"};", 2 },
|
|
/* Top-level `[N]u8` array: zero-init DATAW slot + LEAQ name(SB)
|
|
* addressing for index and address-of. `buf[i] = c` narrows to
|
|
* MOVB; reading back roundtrips through MOVZBQ. */
|
|
{ "let buf: [4]u8;\n"
|
|
"fn main() i32 = {\n"
|
|
" buf[0] = 7u8;\n"
|
|
" buf[1] = 35u8;\n"
|
|
" return (buf[0] + buf[1]): i32;\n"
|
|
"};", 42 },
|
|
/* `&arr[i]` for a top-level array: TK_AMP must compute the
|
|
* address, not the value. Then `*p = c` for *u8 stores 1 byte.
|
|
* Drives Hare's static-buffer pattern (strconv.*tos). */
|
|
{ "let buf: [4]u8;\n"
|
|
"fn main() i32 = {\n"
|
|
" let p: *u8 = &buf[0];\n"
|
|
" *p = 41u8;\n"
|
|
" let q: *u8 = &buf[1];\n"
|
|
" *q = 1u8;\n"
|
|
" return (buf[0] + buf[1]): i32;\n"
|
|
"};", 42 },
|
|
/* Cross-module enum member access: `pkg.Enum.MEMBER`. Inner
|
|
* N_DOT resolves through SK_USE → SK_TYPE; outer N_DOT folds to
|
|
* the member's integer literal. Validates the strconv.base.DEC
|
|
* shape that the *tos / sto* signatures now use. */
|
|
{ "package pkg;\n"
|
|
"export type base = enum i32 { DEC = 10, HEX = 16 };\n"
|
|
"package main;\n"
|
|
"import pkg;\n"
|
|
"fn pick(b: pkg.base) i32 = { return b as i32; };\n"
|
|
"fn main() i32 = {\n"
|
|
" let a: i32 = pick(pkg.base.DEC);\n"
|
|
" let b: i32 = pick(pkg.base.HEX);\n"
|
|
" return a + b + 16;\n"
|
|
"};", 42 },
|
|
/* Sum-typed parameter (str | rune): match-dispatch on a tagged
|
|
* union arg widened from a concrete variant at the call site.
|
|
* Mirrors lib/strings.byteindex's needle parameter. */
|
|
{ "fn pick(n: (str | rune)) i32 = {\n"
|
|
" match (n) {\n"
|
|
" case let s: str => return s.len + 100;\n"
|
|
" case let r: rune => return r: i32;\n"
|
|
" };\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let a: i32 = pick(\"hi\");\n"
|
|
" let b: i32 = pick('?');\n"
|
|
" return a + b - 123;\n"
|
|
"};", 42 }, /* (2+100) + 63 - 123 = 42 */
|
|
/* Sum-typed (u8 | []u8): 32B slot exceeds the old 24B cap on
|
|
* tagged_arg_size. Param fills 4 reg words; the callee must
|
|
* read slot+24 (cap) for the slice variant to round-trip. */
|
|
{ "import bytes;\n"
|
|
"fn main() i32 = {\n"
|
|
" let buf: [4]u8;\n"
|
|
" buf[0] = 1u8; buf[1] = 2u8; buf[2] = 3u8; buf[3] = 4u8;\n"
|
|
" let needle: [2]u8;\n"
|
|
" needle[0] = 3u8; needle[1] = 4u8;\n"
|
|
" let r1: (i32 | void) = bytes.index(buf[0:4], 3u8);\n"
|
|
" let r2: (i32 | void) = bytes.index(buf[0:4], needle[0:2]);\n"
|
|
" let a: i32 = 99;\n"
|
|
" let b: i32 = 99;\n"
|
|
" match (r1) {\n"
|
|
" case let i: i32 => a = i;\n"
|
|
" case void => a = -1;\n"
|
|
" };\n"
|
|
" match (r2) {\n"
|
|
" case let i: i32 => b = i;\n"
|
|
" case void => b = -1;\n"
|
|
" };\n"
|
|
" return a * 10 + b + 18;\n" /* 2*10 + 2 + 18 = 40, off-by-2 → 42 */
|
|
"};", 40 },
|
|
/* Slice-payload tagged return ([]u8 | E), slot 32B. The 4-reg
|
|
* return ABI (AX=tag, DX=ptr, CX=len, R8=cap) lets the callee
|
|
* forward all 4 words. Before the bump, slice.len was dropped
|
|
* because only 3 regs were used. */
|
|
{ "type rterr = !str;\n"
|
|
"fn build(n: i32) ([]u8 | rterr) = {\n"
|
|
" if (n < 0) { return \"bad\": rterr; };\n"
|
|
" let buf: [4]u8;\n"
|
|
" buf[0] = 10u8; buf[1] = 20u8; buf[2] = 30u8; buf[3] = 40u8;\n"
|
|
" return buf[0:n];\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let r: ([]u8 | rterr) = build(3);\n"
|
|
" match (r) {\n"
|
|
" case let xs: []u8 => {\n"
|
|
" if (xs.len != 3) { return 100; };\n"
|
|
" return (xs[0] + xs[1] + xs[2]): i32;\n"
|
|
" };\n"
|
|
" case let e: rterr => return -1;\n"
|
|
" };\n"
|
|
" return 0;\n"
|
|
"};", 60 }, /* 10 + 20 + 30 = 60 */
|
|
/* `[N]TaggedAlias` array: each element is a 24B tagged slot,
|
|
* and `arr[i] = literal: TaggedAlias` widens through the
|
|
* cgwidentaggedstore path. Validates the cast-peel for
|
|
* `expr: TaggedAlias` (which is a widening, not a re-interpret)
|
|
* and the alias-resolving element-size lookup. */
|
|
{ "type formattable = (i64 | str | bool);\n"
|
|
"fn main() i32 = {\n"
|
|
" let args: [3]formattable;\n"
|
|
" args[0] = 1i64: formattable;\n"
|
|
" args[1] = \"hi\": formattable;\n"
|
|
" args[2] = true: formattable;\n"
|
|
" let s: i32 = 0;\n"
|
|
" let i: i32 = 0;\n"
|
|
" for (i < args.len) {\n"
|
|
" match (args[i]) {\n"
|
|
" case let n: i64 => s += n: i32;\n"
|
|
" case let v: str => s += v.len;\n"
|
|
" case let b: bool => { if (b) { s += 39; }; };\n"
|
|
" };\n"
|
|
" i += 1;\n"
|
|
" };\n"
|
|
" return s;\n"
|
|
"};", 42 }, /* 1 + 2 + 39 = 42 */
|
|
/* Hare-style variadic gather: `args: T...` declares an N-arg
|
|
* variadic; the call-site materialises N values into a fresh
|
|
* `[N]T` and synthesises a {ptr,len,cap} slice for the param.
|
|
* Plain element type (i64): no widening, MOVQ-per-element. */
|
|
{ "fn sum(args: i64...) i64 = {\n"
|
|
" let s: i64 = 0i64;\n"
|
|
" let i: i32 = 0;\n"
|
|
" for (i < args.len) { s += args[i]; i += 1; };\n"
|
|
" return s;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" return sum(1i64, 2i64, 3i64, 7i64, 9i64, 20i64): i32;\n"
|
|
"};", 42 },
|
|
/* Variadic with zero args: empty-slice descriptor {nil,0,0}.
|
|
* Confirms the gather path doesn't crash on N=0. */
|
|
{ "fn sum(args: i64...) i64 = {\n"
|
|
" let s: i64 = 0i64;\n"
|
|
" let i: i32 = 0;\n"
|
|
" for (i < args.len) { s += args[i]; i += 1; };\n"
|
|
" return s;\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" let a: i64 = sum();\n"
|
|
" let b: i64 = sum(42i64);\n"
|
|
" return (a + b): i32;\n"
|
|
"};", 42 },
|
|
/* Variadic with tagged-union element type: each gathered arg
|
|
* widens to the variant's slot shape (tag@+0, payload@+8). The
|
|
* runtime match-dispatch reads (i64=1)+(str.len=2)+(bool=39)=42. */
|
|
{ "type formattable = (i64 | str | bool);\n"
|
|
"fn sumtag(args: formattable...) i64 = {\n"
|
|
" let s: i64 = 0i64;\n"
|
|
" let i: i32 = 0;\n"
|
|
" for (i < args.len) {\n"
|
|
" match (args[i]) {\n"
|
|
" case let n: i64 => s += n;\n"
|
|
" case let v: str => s += v.len: i64;\n"
|
|
" case let b: bool => { if (b) { s += 39i64; }; };\n"
|
|
" };\n"
|
|
" i += 1;\n"
|
|
" };\n"
|
|
" return s;\n"
|
|
"};\n"
|
|
"fn main() i32 = { return sumtag(1i64, \"hi\", true): i32; };", 42 },
|
|
/* Variadic forwarding: `wrap(args...)` passes the local slice
|
|
* directly to `sum`, no re-gather. Mirrors Hare's wrapper shape
|
|
* (`fn println(args: formattable...) = fprintln(os.stdout, args...)`). */
|
|
{ "fn sum(args: i64...) i64 = {\n"
|
|
" let s: i64 = 0i64;\n"
|
|
" let i: i32 = 0;\n"
|
|
" for (i < args.len) { s += args[i]; i += 1; };\n"
|
|
" return s;\n"
|
|
"};\n"
|
|
"fn wrap(prefix: i64, args: i64...) i64 = {\n"
|
|
" return prefix + sum(args...);\n"
|
|
"};\n"
|
|
"fn main() i32 = {\n"
|
|
" return wrap(2i64, 1i64, 2i64, 3i64, 4i64, 5i64, 7i64, 18i64): i32;\n"
|
|
"};", 42 },
|
|
/* lib/fmt user-side: `fmt.println(args: formattable...)` gathers
|
|
* mixed-type args at the call site. End-to-end exercises the
|
|
* lib/fmt graduation: the wrapper-chain `println → fprintln →
|
|
* fprint` over an io.handle file arm is itself variadic-forwarding,
|
|
* so this validates both gather (at main) and `args...` forward
|
|
* (inside lib/fmt). println returns Hare's `(size | io.error)`
|
|
* (#94 fold-eFinal); the size arm carries bytes printed
|
|
* (`hello 7\n` = 8). */
|
|
{ "import fmt;\n"
|
|
"import io;\n"
|
|
"fn main() i32 = {\n"
|
|
" match (fmt.println(\"hello\", 7i64)) {\n"
|
|
" case let n: size => return n: i32;\n"
|
|
" case let _e: io.error => return -1;\n"
|
|
" };\n"
|
|
"};", 8 },
|
|
/* short-circuit `&&`: RHS skipped when LHS is false. Without
|
|
* short-circuit the `p.x` deref on a nil pointer segfaults.
|
|
* Pinned the cgen bug surfaced by lib/getopt's argv guards
|
|
* (`argslen > 0 && !streq(argsptr[0], "--")`) on a nil argsptr. */
|
|
{ "type point = struct { x: i32, y: i32 };\n"
|
|
"fn main() i32 = {\n"
|
|
" let p: *point = nil;\n"
|
|
" if (p != nil && p.x > 0) { return 1; };\n"
|
|
" return 42;\n"
|
|
"};", 42 },
|
|
/* short-circuit `||`: RHS skipped when LHS is true. */
|
|
{ "type point = struct { x: i32, y: i32 };\n"
|
|
"fn main() i32 = {\n"
|
|
" let p: *point = nil;\n"
|
|
" if (p == nil || p.x > 0) { return 42; };\n"
|
|
" return 1;\n"
|
|
"};", 42 },
|
|
/* `&&` LHS true: RHS evaluated, expression yields its boolean. */
|
|
{ "type point = struct { x: i32, y: i32 };\n"
|
|
"fn main() i32 = {\n"
|
|
" let pt: point = point { x = 5, y = 10 };\n"
|
|
" let p: *point = &pt;\n"
|
|
" if (p != nil && p.x > 0) { return 42; };\n"
|
|
" return 1;\n"
|
|
"};", 42 },
|
|
/* `||` LHS false: RHS evaluated, expression yields its boolean. */
|
|
{ "type point = struct { x: i32, y: i32 };\n"
|
|
"fn main() i32 = {\n"
|
|
" let pt: point = point { x = 7, y = 0 };\n"
|
|
" let p: *point = &pt;\n"
|
|
" if (p == nil || p.x > 0) { return 42; };\n"
|
|
" return 1;\n"
|
|
"};", 42 },
|
|
/* mixed `&&` / `||` precedence — `&&` binds tighter than `||`,
|
|
* so `(p != nil && p.x > 0) || p == nil`. With short-circuit at
|
|
* each level: AND skips `p.x` (LHS false), OR keeps the true. */
|
|
{ "type point = struct { x: i32, y: i32 };\n"
|
|
"fn main() i32 = {\n"
|
|
" let p: *point = nil;\n"
|
|
" if (p != nil && p.x > 0 || p == nil) { return 42; };\n"
|
|
" return 1;\n"
|
|
"};", 42 },
|
|
/* short-circuit must still yield a clean boolean in the
|
|
* expression context (not just inside `if`). `true && false`
|
|
* stored into a bool and re-checked. */
|
|
{ "fn main() i32 = {\n"
|
|
" let a: bool = (1 > 0) && (2 < 1);\n"
|
|
" let b: bool = (1 < 0) || (2 > 1);\n"
|
|
" let n: i32 = 0;\n"
|
|
" if (!a) { n += 10; };\n"
|
|
" if (b) { n += 32; };\n"
|
|
" return n;\n"
|
|
"};", 42 },
|
|
{ NULL, 0 }
|
|
};
|
|
|
|
int
|
|
main(void)
|
|
{
|
|
const char *bin = getenv("BIN");
|
|
if (!bin) bin = "out/bin";
|
|
/* Resolve to absolute path: tests chdir into /tmp/... */
|
|
char absbin[1024];
|
|
if (bin[0] != '/') {
|
|
char cwd[1024];
|
|
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
|
|
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
|
|
bin = absbin;
|
|
}
|
|
|
|
int n = 0, fail = 0;
|
|
for (int i = 0; rows[i].src; i++, n++) {
|
|
char tmpdir[64];
|
|
snprintf(tmpdir, sizeof tmpdir, "/tmp/wwe2e_%d_d_%d", getpid(), i);
|
|
if (mkdir(tmpdir, 0755) != 0) {
|
|
fprintf(stderr, "row %d: mkdir %s: %s\n",
|
|
i, tmpdir, strerror(errno));
|
|
fail++;
|
|
continue;
|
|
}
|
|
|
|
/* Source, output, and the caller-owned output `.sepwork` all live
|
|
* inside this invocation-owned directory. */
|
|
char src[128], outbin[128], sepdir[160], rmcmd[192];
|
|
snprintf(src, sizeof src, "%s/wwe2e_%d_%d.ww", tmpdir, getpid(), i);
|
|
snprintf(outbin, sizeof outbin, "%s/wwe2e_%d_%d", tmpdir, getpid(), i);
|
|
snprintf(sepdir, sizeof sepdir, "%s.sepwork", outbin);
|
|
snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", sepdir);
|
|
int row_failed = 0;
|
|
char cmd[1024];
|
|
int got;
|
|
|
|
FILE *f = fopen(src, "wb");
|
|
if (!f) {
|
|
fprintf(stderr, "row %d: fopen %s: %s\n",
|
|
i, src, strerror(errno));
|
|
row_failed = 1;
|
|
goto cleanup;
|
|
}
|
|
wwtest_fputs(rows[i].src, f);
|
|
if (ferror(f)) {
|
|
fprintf(stderr, "row %d: write %s failed\n", i, src);
|
|
row_failed = 1;
|
|
}
|
|
if (fclose(f) != 0) {
|
|
fprintf(stderr, "row %d: close %s: %s\n",
|
|
i, src, strerror(errno));
|
|
row_failed = 1;
|
|
}
|
|
if (row_failed) goto cleanup;
|
|
|
|
snprintf(cmd, sizeof cmd, "%s/ww build -o %s %s",
|
|
bin, outbin, src);
|
|
if (runwait(cmd) != 0) {
|
|
row_failed = 1;
|
|
goto cleanup;
|
|
}
|
|
|
|
got = runwait(outbin);
|
|
if (got != rows[i].want_exit) {
|
|
fprintf(stderr, "row %d: exit %d, want %d\n src: %s\n",
|
|
i, got, rows[i].want_exit, rows[i].src);
|
|
row_failed = 1;
|
|
}
|
|
|
|
cleanup:
|
|
{
|
|
int cleanup_failed = 0;
|
|
if (runwait(rmcmd) != 0) {
|
|
fprintf(stderr, "row %d: remove %s failed\n", i, sepdir);
|
|
cleanup_failed = 1;
|
|
}
|
|
if (unlink(src) != 0 && errno != ENOENT) {
|
|
fprintf(stderr, "row %d: unlink %s: %s\n",
|
|
i, src, strerror(errno));
|
|
cleanup_failed = 1;
|
|
}
|
|
if (unlink(outbin) != 0 && errno != ENOENT) {
|
|
fprintf(stderr, "row %d: unlink %s: %s\n",
|
|
i, outbin, strerror(errno));
|
|
cleanup_failed = 1;
|
|
}
|
|
if (rmdir(tmpdir) != 0) {
|
|
fprintf(stderr, "row %d: rmdir %s: %s\n",
|
|
i, tmpdir, strerror(errno));
|
|
cleanup_failed = 1;
|
|
}
|
|
if (!row_failed && cleanup_failed) row_failed = 1;
|
|
}
|
|
if (row_failed) fail++;
|
|
}
|
|
if (fail) { fprintf(stderr, "%d/%d e2e tests failed\n", fail, n); return 1; }
|
|
printf("e2e: %d/%d ok\n", n, n);
|
|
return 0;
|
|
}
|