diff --git a/build_selfhost_native.py b/build_selfhost_native.py index 7fdbb8c1..f6291007 100644 --- a/build_selfhost_native.py +++ b/build_selfhost_native.py @@ -95,6 +95,7 @@ # + rax 回存;内置体名索引扫描段留 elf.cr 原位);跨轴消费 x86_64/instr.cr # 编码原语(e2_mov/e2_w8/e2_store_ret)。 os_linux_files = [ + 'src/os/linux/file_mode.cr', 'src/os/linux/entry.cr', 'src/os/linux/callseq.cr', 'src/os/linux/syscall.cr', diff --git a/docs/superpowers/plans/2026-10-24-multibranch-v10.md b/docs/superpowers/plans/2026-10-24-multibranch-v10.md new file mode 100644 index 00000000..a9133294 --- /dev/null +++ b/docs/superpowers/plans/2026-10-24-multibranch-v10.md @@ -0,0 +1,220 @@ +# v10 多路分支 / match 决策 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 在 v10 `.ccr` 上实现多路 `MATCH` 节点和 CASE 表:corec 把 `match` 降为多路分支,corearch 映射为比较链/二分/跳转表,interp 与 ELF 语义一致。 + +**Architecture:** 保留 `IR_BRANCH` 作为 `if`/`while` 的二元分支;新增 `IR_MATCH` 和 CASE 记录。CASE 是 v10 第 9 段,记录为 24B `{value, kind, str_index, target_nod, flags}`。先做整数/bool/enum 标签的多路分派;字符串/struct/ident 模式在后续任务补齐。 + +**Spec:** `docs/superpowers/specs/2026-10-23-pattern-matching-ir-design.md`(v9.1 设计,本计划适配 v10)+ `docs/superpowers/specs/2026-10-23-lattice-ccr-v10-design.md` §11 CASE。 + +## 全局约束 + +- 不改变 `IR_BRANCH` 的二元编码;`if`/`while` 继续走现有路径。 +- `MATCH` 只表示多路分派;`CASE` 不存宽度、偏移、ABI 或机器码。 +- CASE 值只进入 v10 CASE 段;字符串模式后续归一化为 `str_eq`,不把字符串指针放进 CASE。 +- 每个任务单独提交;先 CASE/IR_MATCH 读写,再 corec,再 corearch/interp。 +- 自举要求 stage2/stage3 逐字节一致;`test_bootstrap` 和 canary 不因中间态长时间红。 +- `jj` only;子代理每个任务新开,禁止并行实现同一文件。 +- 所有长命令 `nice -n 19`。 + +--- + +### Task 1: IR_MATCH/CASE schema 与 v10 读写 + +**Files:** +- Modify: `src/compiler/ast.cr` +- Modify: `src/compiler/globals.cr` +- Modify: `src/compiler/ccr_schema.cr` +- Modify: `src/compiler/ccr_io.cr` +- Test: `tests/selfhost/test_multibranch_v10.py` + +**Interfaces:** +- Consumes: v10 CASE 段 tag 9,24B 记录。 +- Produces: + - `IR_MATCH : int = 54` + - `g_ccr_case : string`, `g_ccr_case_count : int` + - CASE record offsets/stride: + - `CCR_CASE_VALUE=0` (8B signed) + - `CCR_CASE_KIND=8` (4B) + - `CCR_CASE_STR=12` (4B signed) + - `CCR_CASE_TARGET=16` (4B signed NOD label id) + - `CCR_CASE_FLAGS=20` (4B) + - `CCR_CASE_DISK=24` + - `CCR_CASE_KIND_VALUE=0`, `KIND_ENUM=1`, `KIND_STRING=2`, `KIND_DEFAULT=3` + +- [ ] **Step 1: failing test** + +Create a Python test that builds a `.ccr` with an `IR_MATCH` node and 3 CASE records, parses the CASE segment independently, and asserts count/order/value/target. Also assert a v10 file with CASE count > 0 is not rejected by the container (once implemented). + +- [ ] **Step 2: implement schema** + +Add `IR_MATCH`; `ccr_op_is_known` includes it; CASE segment writer reads `g_ccr_case`; loader rebuilds it. For now `g_ccr_case_count=0` unless corec populates. CASE segment size = `4 + count*24`. Add bounds/str/target checks; default exactly one optional. + +- [ ] **Step 3: verify** + +```bash +python3 tests/selfhost/test_multibranch_v10.py +python3 tests/selfhost/test_ccr_v10_format.py +python3 tests/selfhost/test_ccr_v10_stream.py +nice -n 19 python3 build_selfhost_native.py +``` + +- [ ] **Step 4: commit** + +`jj commit -m "ccr: add v10 match and case schema"` + +--- + +### Task 2: corec `match` 降为 MATCH + +**Files:** +- Modify: `src/compiler/ir_gen.cr` +- Modify: `src/compiler/dump.cr` +- Modify: `src/compiler/ccr_types.cr` +- Modify: `src/compiler/cir_cache.cr` +- Test: `tests/selfhost/test_multibranch_v10.py` + +**Interfaces:** +- Consumes: Task 1 `IR_MATCH`, `g_ccr_case`. +- Produces: one `IR_MATCH` for a match whose arms are int/bool/enum/wildcard; body labels are existing labels; default is a `KIND_DEFAULT` case. +- Unsupported patterns keep the existing sequential lowering until Task 4; do not silently change semantics. + +- [ ] **Step 1: RED fixture** + +4-way int match and enum match; assert exactly one `MATCH` in the function and CASE count = arms + default; old code produces zero. + +- [ ] **Step 2: implement** + +Collect cases in arm order, dedupe by value keeping first, `KIND_DEFAULT` for `_`; emit `MATCH` with `src1=scrutinee`, `src2=case_start`, `src3=case_count`. Keep optional representation special path unchanged for now. If a match has string/struct/ident patterns, fall back to existing code and mark a TODO; do not emit a wrong MATCH. + +- [ ] **Step 3: CIR cache** + +Disable CIR cache for functions containing `MATCH` in this task (simplest correct); add a per-function `g_cir_has_match` reset and check. Task 5 may replace with CASE range persistence. + +- [ ] **Step 4: dump** + +`--dump-objects` prints `match` with case range and cases. + +- [ ] **Step 5: verify** + +```bash +python3 tests/selfhost/test_multibranch_v10.py +nice -n 19 python3 tests/selfhost/test_ccr_types.py +nice -n 19 python3 build_selfhost_native.py +./build/corec check src/compiler +``` + +- [ ] **Step 6: commit** + +`jj commit -m "ccr: lower integer and enum matches to v10 MATCH"` + +--- + +### Task 3: corearch / interp / dataflow support + +**Files:** +- Modify: `src/arch/x86_64/instr.cr` +- Modify: `src/arch/x86_64/regalloc.cr` +- Modify: `src/arch/x86_64/sizes.cr` if present +- Modify: `src/compiler/dataflow.cr` +- Modify: `src/compiler/interp.cr` +- Modify: `src/compiler/ccr_io.cr` validation +- Test: `tests/selfhost/test_multibranch_v10.py` + +**Interfaces:** +- `MATCH` successors = all case targets (plus default); regalloc handles multiple successors. +- corearch emits: + - count 0 -> default jump/trap + - count 1..2 -> cmp/jcc chain + - count >2 -> binary search by sorted value (Task 4 can replace with jump table) +- interp reads the loaded case table and jumps to the target; default on no hit. +- `MATCH` target labels must be valid NOD labels and in function span; loader validates. + +- [ ] **Step 1: RED** 4-way int match ELF and interp; both must return the selected arm; current no MATCH path fails. +- [ ] **Step 2: implement**; keep `IR_BRANCH` code untouched. +- [ ] **Step 3: verify**: +```bash +python3 tests/selfhost/test_multibranch_v10.py +python3 tests/selfhost/test_interp_parity.py +python3 tests/selfhost/test_native_memory.py +python3 tests/selfhost/test_backend_bootstrap.py +``` +- [ ] **Step 4: commit** +`jj commit -m "backend: lower v10 MATCH to compare chains"` + +--- + +### Task 4: dense jump table / sparse binary search + +**Files:** +- Modify: `src/arch/x86_64/instr.cr` +- Modify: `src/arch/x86_64/regalloc.cr` +- Modify: `src/arch/x86_64/sizes.cr` +- Test: `tests/selfhost/test_multibranch_v10.py` + +**Interfaces:** +- Dense: `count >= 8 && max-min+1 <= 2*count` -> 32-bit relative table in rodata; range check; default out-of-range. +- Sparse: sorted binary comparison tree. +- `s3==1` with value 1 and default remains same machine shape as existing `IR_BRANCH` if possible. +- Add 16/64-way benchmarks and assert no linear chain on dense case. + +- [ ] **Step 1: RED** 64-way dense fixture; assert ELF contains table path / bounded comparisons. +- [ ] **Step 2: implement** with label patch mechanism; no PIE/dynamic relocations. +- [ ] **Step 3: verify and commit** +`ccr: add jump-table lowering for dense match` + +--- + +### Task 5: pattern coverage + +**Files:** +- Modify: `src/compiler/parser.cr` (tuple pattern if missing) +- Modify: `src/compiler/checker.cr` +- Modify: `src/compiler/ir_gen.cr` +- Modify: `bootstrap/corec/frontend/desugar.py` +- Test: `tests/selfhost/test_multibranch_v10.py` + +**Interfaces:** +- string/char patterns normalize to `str_eq` and binary MATCH/default. +- struct/tuple fields load then recursively dispatch. +- ident binds; wildcard default. +- literal exhaustiveness: `_` or irrefutable arm else `EC_TM_EXHAUST`. +- corec source must avoid unsupported bootstrap match patterns; extend desugar if needed. + +- [ ] **Steps** TDD per pattern: string, char, struct, tuple, ident; behavior parity; no silent first-arm. +- [ ] **Commit** `ccr: cover string struct and ident match patterns` + +--- + +### Task 6: cleanup, cache, canary + +**Files:** +- Modify: `src/compiler/cir_cache.cr` (persist CASE range or keep disable) +- Modify: `tools/baseline/canary_values.tsv` +- Modify: `docs/superpowers/specs/2026-10-23-pattern-matching-ir-design.md` (v10 adaptation) +- Test: `tests/selfhost/test_multibranch_v10.py` + +- [ ] Replace temporary cache disable with CASE-range persistence if feasible; otherwise document. +- [ ] Update bootstrap parser for V4 syntax tests if CI still red. +- [ ] Run full gate and relock only affected `.ccr`/ELF with attribution. +- [ ] Commit. + +## Final Gate + +```bash +nice -n 19 python3 build_selfhost_native.py +./build/corec check src/compiler +python3 tests/selfhost/test_multibranch_v10.py +python3 tests/selfhost/test_ccr_v10_format.py +python3 tests/selfhost/test_ccr_v10_stream.py +python3 tests/selfhost/test_ccr_types.py +python3 tests/selfhost/test_backend_bootstrap.py +python3 tests/selfhost/test_interp_parity.py +python3 tests/selfhost/test_native_memory.py +python3 tests/harness/test_ci_hook_coverage.py +bash tools/baseline/canary_check.sh +./build/corec build src/compiler/main.cr -o build/corec2 --static -O 0 +./build/corec2 build src/compiler/main.cr -o build/corec3 --static -O 0 +cmp build/corec2 build/corec3 +``` diff --git a/docs/superpowers/specs/2026-10-23-pattern-matching-ir-design.md b/docs/superpowers/specs/2026-10-23-pattern-matching-ir-design.md index c84aa06d..f4dad782 100644 --- a/docs/superpowers/specs/2026-10-23-pattern-matching-ir-design.md +++ b/docs/superpowers/specs/2026-10-23-pattern-matching-ir-design.md @@ -1,5 +1,13 @@ # match 的决策树编译与统一分支节点 +> **v10 实现说明(2026-10-24)**:现行 `.ccr` 是 v10,不是本文写作时的 v9.1。 +> 多路分支在 v10 中使用 `IR_MATCH`(opcode 54)+ CASE 24B 记录 +> `{value, kind, str_index, target_nod, flags}`;`IR_BRANCH` 继续承载二元 +> `if`/`while`。本文的 v9.1 版本/段表/16B CASE 段落是历史设计,已被 +> `docs/superpowers/specs/2026-10-23-lattice-ccr-v10-design.md` §11 取代。 +> 已实现范围:int/bool/enum/string/char/struct/tuple/ident 模式、二分与密集 +> 跳转表、CIR CASE 区间持久化。下文保留历史决策与推导。 + ## 概述 `match` 目前在 `src/compiler/ir_gen.cr:2683` 起按 arm 顺序展开为 `IR_CONST`、`IR_BINARY`、`IR_BRANCH` 组成的比较链,corec 的 IR 中只有二元条件分支。字符串、字符、结构体、标识符模式在 `ir_gen` 中没有测试或绑定生成,会静默执行第一个 arm。 diff --git a/src/arch/x86_64/backend_arrays.cr b/src/arch/x86_64/backend_arrays.cr index 1c0d1e67..0006192b 100644 --- a/src/arch/x86_64/backend_arrays.cr +++ b/src/arch/x86_64/backend_arrays.cr @@ -215,6 +215,35 @@ fn grow_rodataref(needed: int): g_x86_rodataref_ro = n2 g_x86_rodataref_cap = nc +fn grow_match_table_ref(needed: int): + if needed < g_match_table_ref_cap: + return + nc : ., mut = g_match_table_ref_cap * 2 + if nc < 32: + nc = 32 + if nc < needed: + nc = needed + 32 + n1 := alloc(nc * 8) + _dyncpy(g_match_table_ref_pos, g_match_table_ref_cap * 8, n1) + g_match_table_ref_pos = n1 + n2 := alloc(nc * 8) + _dyncpy(g_match_table_ref_id, g_match_table_ref_cap * 8, n2) + g_match_table_ref_id = n2 + g_match_table_ref_cap = nc + +fn grow_label_abs(needed: int): + if needed < g_label_abs_cap: + return + nc : ., mut = g_label_abs_cap * 2 + if nc < 64: + nc = 64 + if nc < needed: + nc = needed + 64 + nb := alloc(nc * 8) + _dyncpy(g_label_abs, g_label_abs_cap * 8, nb) + g_label_abs = nb + g_label_abs_cap = nc + fn grow_alloc_patch(needed: int): if needed < g_x86_alloc_patch_cap: return diff --git a/src/arch/x86_64/backend_flags.cr b/src/arch/x86_64/backend_flags.cr index 529f36d2..514f974a 100644 --- a/src/arch/x86_64/backend_flags.cr +++ b/src/arch/x86_64/backend_flags.cr @@ -23,6 +23,7 @@ fn register_backend_flags(): cli_flag_bool_debug("inject-read-gap", "", "Debug hook: truncate last version interval") cli_flag_bool_debug("inject-coexist-oob", "", "Debug hook: probe entries_coexist out of bounds") cli_flag_bool_debug("dump-objects", "", "Debug: dump NOD/EDG objects") + cli_flag_bool_debug("dump-match-plan", "", "Debug: dump MATCH dispatch strategy") cli_flag_bool_debug("dump-nod-types", "", "Debug: dump NOD type_id/flags") cli_flag_bool_debug("dump-alloc-strides", "", "Debug: dump ALLOC_ARRAY element type/stride") cli_flag_bool_debug("dump-types", "", "Debug: dump TYPE segment") diff --git a/src/arch/x86_64/backend_globals.cr b/src/arch/x86_64/backend_globals.cr index a4d3cceb..3cb7784d 100644 --- a/src/arch/x86_64/backend_globals.cr +++ b/src/arch/x86_64/backend_globals.cr @@ -66,6 +66,26 @@ g_x86_alloc_patch_pos : string, mut g_x86_alloc_patch_cap : int, mut g_x86_alloc_patch_count : int, mut g_x86_emit_rt_stubs : int, mut // 1 = pure-static: backend emits g_set_curg/g_get_curg stubs +// Count of IR_MATCH nodes that actually entered e2_emit_match_binary. This is a +// verification counter, not a strategy predicate: a scratch-only emitter pass in +// --dump-match-plan and the real Phase 3 emission both increment it. +g_match_bin_emits : int, mut +// Dense jump-table state. g_match_table_cursor is the table ordinal during code +// emission; each emitted LEA is recorded with that ordinal and patched only when +// the rodata writer has the final table address. This cannot use a +// g2_rodata_sz()-based offset: Phase 3 can still intern BigInt decimal strings. +g_match_table_emits : int, mut +g_match_table_cursor : int, mut +g_match_table_ro_cursor : int, mut +g_match_table_ref_pos : string, mut +g_match_table_ref_id : string, mut +g_match_table_ref_count : int, mut +g_match_table_ref_cap : int, mut +// Label number -> absolute code-buffer position. g_label_poses is per-function +// and overwritten; case targets are global label numbers, so the rodata phase +// needs this global map to resolve table entries. +g_label_abs : string, mut +g_label_abs_cap : int, mut // --big-int: user int slots hold tagged values (immediate v*2 or BigInt pointer+1). // The compiler itself and the BigInt runtime blob stay raw. See int_rep.cr. g_x86_big_enabled : int, mut diff --git a/src/arch/x86_64/corearch.cr b/src/arch/x86_64/corearch.cr index c16bd13f..074318c9 100644 --- a/src/arch/x86_64/corearch.cr +++ b/src/arch/x86_64/corearch.cr @@ -284,9 +284,125 @@ fn dump_object_surface(): print(" ec ") print_i(nod_edge_count(ni)) println("") + if nod_op(ni) == IR_MATCH: + // CASE rows are part of the match node's dumped surface. The range + // comes from the MATCH operands; targets are NOD ids of LABEL nodes. + mstart := nod_s2(ni) + mcount := nod_s3(ni) + mi3 : ., mut = 0 + loop: + if mi3 >= mcount: + break + cbase := (mstart + mi3) * CCR_V10_CASE_DISK + print("case ") + print_i(mstart + mi3) + print(" value ") + print_i(cell_load(g_ccr_case, cbase + CCR_CASE_VALUE)) + print(" kind ") + print_i(cell_load32(g_ccr_case, cbase + CCR_CASE_KIND)) + print(" str ") + print_i(cell_load_i32(g_ccr_case, cbase + CCR_CASE_STR)) + print(" target ") + print_i(cell_load_i32(g_ccr_case, cbase + CCR_CASE_TARGET)) + print(" flags ") + print_i(cell_load32(g_ccr_case, cbase + CCR_CASE_FLAGS)) + println("") + mi3 = mi3 + 1 nod_inputs(ni) // print outgoing edge records through the kernel helper ni = ni + 1 +// --dump-match-plan: per IR_MATCH, report the CASE row count, the strategy +// predicate, and how many MATCH nodes actually entered e2_emit_match_binary. +// `binary_emits` is the verification channel: it is incremented inside the real +// emitter, not in the predicate, so a strategy/path mismatch shows up as +// `strategy binary` with `binary_emits 0`. +// +// The read-only guarantee is about artifacts: this runs the real emitter once +// per MATCH into a throwaway buffer and writes no ELF, no .ccr, and no source. +// A label table of -1 forces every case hit through pending backpatching, so no +// absolute target needs to exist yet. +fn dump_match_plan(): + g_match_bin_emits = 0 + g_match_table_emits = 0 + g_match_table_cursor = 0 + g_match_table_ref_count = 0 + g_pending_count = 0 + // Mirror elf_gen's pre-emission global marking so e2_load_var's global/local + // dispatch reads an initialized table during the scratch pass. + grow_is_global(g_ir_var_count) + gi : ., mut = 0 + loop: + if gi >= g_ir_global_count: + break + gv := cell_load(g_ir_globals, gi * 24 + 8) + if gv >= 0: + grow_is_global(gv + 1) + cell_store(g_x86_is_global, gv * 8, 1) + gi = gi + 1 + max_lbl : ., mut = 0 + ni : ., mut = 0 + loop: + if ni >= g_v7_nod_count: + break + if nod_op(ni) == IR_MATCH: + ms := nod_s2(ni) + mc := nod_s3(ni) + cj : ., mut = 0 + loop: + if cj >= mc: + break + ct := cell_load_i32(g_ccr_case, (ms + cj) * CCR_V10_CASE_DISK + CCR_CASE_TARGET) + if ct >= 0 && ct < g_v7_nod_count: + if nod_op(ct) == IR_LABEL: + ln := nod_s1(ct) + if ln + 1 > max_lbl: + max_lbl = ln + 1 + cj = cj + 1 + ni = ni + 1 + if max_lbl > 0: + grow_label_poses(max_lbl) + lj : ., mut = 0 + loop: + if lj >= max_lbl: + break + cell_store(g_label_poses, lj * 8, -1) + lj = lj + 1 + + ni = 0 + loop: + if ni >= g_v7_nod_count: + break + if nod_op(ni) == IR_MATCH: + ms := nod_s2(ni) + mc := nod_s3(ni) + // Scratch-only real-emitter pass: the returned byte count is not + // used, only the side effects that the channel reports. + scratch := alloc(mc * 32 + 256) + emit_instr(ni, scratch, 0) + print("match ") + print_i(ni) + print(" cases ") + print_i(mc) + print(" nondefault ") + print_i(e2_match_nondefault_count(ms, mc)) + print(" strategy ") + if e2_match_table_span(ms, mc) > 0: + print("table") + else if e2_match_use_binary(ms, mc) != 0: + print("binary") + else: + print("linear") + println("") + ni = ni + 1 + print("binary_emits ") + print_i(g_match_bin_emits) + println("") + print("table_emits ") + print_i(g_match_table_emits) + println("") + // Scratch pass records refs but never patches them; drop them. + g_match_table_ref_count = 0 + // --dump-nod-types: print each node's semantic type_id and feature flags. The // disk has no term-index field in v10, so this channel exposes the actual wire // semantics. It returns before emission. @@ -451,6 +567,12 @@ fn corearch_main() -> int: dump_object_surface() return 0 + // Hidden channel: per-MATCH dispatch strategy. It shares the emitter's + // predicate and returns before O2 allocation or emission. + if cli_has("dump-match-plan") != 0: + dump_match_plan() + return 0 + // O2 allocation and consistency checks run here, not in corec. The .ccr no // longer carries REG_ASSIGN or ENT. A violation is a compile error. // The instance declaration controls this: x86 has needs_alloc/needs_verify = 1 @@ -551,7 +673,7 @@ fn corearch_main() -> int: g_elf_buf = alloc(16777216) sz := elf_gen(g_elf_buf) w16(g_elf_buf, 16, 3) - fd := syscall3(2, out_path, 577, 493) + fd := syscall3(2, out_path, 577, linux_mode_rw()) if fd < 0: print("error: cannot write ") println(out_path) @@ -644,7 +766,7 @@ fn corearch_main() -> int: return 1 // Pure static: write directly; the frontend prepends rt.cr. ELF output // is executable (0755); .so output stays 0644. - fd := syscall3(2, out_path, 577, 493) + fd := syscall3(2, out_path, 577, linux_mode_exec()) if fd < 0: print("error: cannot write ") println(out_path) @@ -667,7 +789,7 @@ fn corearch_main() -> int: return 1 else: sz := elf_gen(g_elf_buf) - fd := syscall3(2, out_path, 577, 493) // 0755:可执行输出(同静态路径修复) if fd < 0 { print("error: cannot write "); println(out_path); return 1; } + fd := syscall3(2, out_path, 577, linux_mode_exec()) // 0755:可执行输出(同静态路径修复) if fd < 0 { print("error: cannot write "); println(out_path); return 1; } syscall3(1, fd, g_elf_buf, sz) syscall3(3, fd, 0, 0) print(" -> ") diff --git a/src/arch/x86_64/instr.cr b/src/arch/x86_64/instr.cr index 048ccdf7..66f437aa 100644 --- a/src/arch/x86_64/instr.cr +++ b/src/arch/x86_64/instr.cr @@ -574,6 +574,431 @@ fn e2_st_var(buf: string, pos: int, reg: int, var_idx: int) -> int: fn e2_rslot(r: int) -> int: return E2_REG_SLOT_BASE + r +// ── v10 sparse MATCH: balanced binary search ────────────────────────────── +// +// A CASE range with more than two non-default rows is dispatched through a +// balanced binary decision tree instead of the linear cmp/je chain. Nodes are +// laid out in pre-order and every internal node is a fixed 30-byte block: +// +// movabs r11, imm64 (10) +// cmp r10, r11 (3) +// je hit (6) +// jl lower (6) +// jmp upper (5) +// +// Fixed-size nodes make child positions pure arithmetic, so internal edges are +// patched directly. Case hits and the DEFAULT miss still go through the +// g_pending_* label backpatch table; no new address surface is introduced. +// Jump-table/rodata lowering is deliberately deferred. + +E2_MATCH_NODE_SZ : int = 30 + +fn e2_match_nondefault_count(s2: int, s3: int) -> int: + n : ., mut = 0 + i : ., mut = 0 + loop: + if i >= s3: + break + kind := cell_load32(g_ccr_case, (s2 + i) * CCR_V10_CASE_DISK + CCR_CASE_KIND) + if kind != CCR_CASE_KIND_DEFAULT: + n = n + 1 + i = i + 1 + return n + +// The single strategy predicate. The emitter and the --dump-match-plan channel +// both call this, so the observable plan cannot drift from the emitted code. +fn e2_match_use_binary(s2: int, s3: int) -> int: + if e2_match_nondefault_count(s2, s3) > 2: + return 1 + return 0 + +fn e2_match_idx_less(s2: int, a: int, b: int) -> int: + // Compare by value, then by original CASE-relative index. The index + // tie-break makes the order total, so duplicate values deterministically + // keep the first (lowest-index) case. + va := cell_load(g_ccr_case, (s2 + a) * CCR_V10_CASE_DISK + CCR_CASE_VALUE) + vb := cell_load(g_ccr_case, (s2 + b) * CCR_V10_CASE_DISK + CCR_CASE_VALUE) + if va < vb: + return 1 + if va > vb: + return 0 + if a < b: + return 1 + return 0 + +// Collect the non-default rows into idx, sort them with an in-place shell sort, +// and drop duplicate values (first case wins). Returns the unique count. +fn e2_match_sort_cases(s2: int, s3: int, idx: string) -> int: + n : ., mut = 0 + i : ., mut = 0 + loop: + if i >= s3: + break + kind := cell_load32(g_ccr_case, (s2 + i) * CCR_V10_CASE_DISK + CCR_CASE_KIND) + if kind != CCR_CASE_KIND_DEFAULT: + cell_store(idx, n * 8, i) + n = n + 1 + i = i + 1 + + gap : ., mut = n / 2 + loop: + if gap <= 0: + break + i = gap + loop: + if i >= n: + break + tmp := cell_load(idx, i * 8) + j : ., mut = i + loop: + if j < gap: + break + prev := cell_load(idx, (j - gap) * 8) + if e2_match_idx_less(s2, tmp, prev) == 0: + break + cell_store(idx, j * 8, prev) + j = j - gap + cell_store(idx, j * 8, tmp) + i = i + 1 + gap = gap / 2 + + uniq : ., mut = 0 + i = 0 + loop: + if i >= n: + break + ci := cell_load(idx, i * 8) + keep : ., mut = 1 + if uniq > 0: + prev_i := cell_load(idx, (uniq - 1) * 8) + if cell_load(g_ccr_case, (s2 + ci) * CCR_V10_CASE_DISK + CCR_CASE_VALUE) == cell_load(g_ccr_case, (s2 + prev_i) * CCR_V10_CASE_DISK + CCR_CASE_VALUE): + keep = 0 + if keep != 0: + cell_store(idx, uniq * 8, ci) + uniq = uniq + 1 + i = i + 1 + return uniq + +fn e2_match_case_label(ctarget: int) -> int: + if ctarget >= 0 && ctarget < g_ir_instr_count: + if iri_op(ctarget) == IR_LABEL: + return iri_s1(ctarget) + return -1 + +fn e2_match_patch_jcc(buf: string, jmp_pos: int, label_no: int): + if label_no < 0: + return + if cell_load(g_label_poses, label_no * 8) >= 0: + e2_big_patch_jcc(buf, jmp_pos, cell_load(g_label_poses, label_no * 8)) + else: + grow_pending(g_pending_count + 1) + cell_store(g_pending_pos, g_pending_count * 8, jmp_pos + 2) + cell_store(g_pending_label, g_pending_count * 8, label_no) + g_pending_count = g_pending_count + 1 + +fn e2_match_patch_jmp(buf: string, jmp_pos: int, label_no: int): + if label_no < 0: + return + if cell_load(g_label_poses, label_no * 8) >= 0: + e2_big_patch_jmp(buf, jmp_pos, cell_load(g_label_poses, label_no * 8)) + else: + grow_pending(g_pending_count + 1) + cell_store(g_pending_pos, g_pending_count * 8, jmp_pos + 1) + cell_store(g_pending_label, g_pending_count * 8, label_no) + g_pending_count = g_pending_count + 1 + +// A miss edge goes to DEFAULT when one exists, otherwise to the ud2 trap. +fn e2_match_patch_miss(buf: string, jmp_pos: int, is_jcc: int, default_lbl: int, trap_pos: int): + if default_lbl >= 0: + if is_jcc != 0: + e2_match_patch_jcc(buf, jmp_pos, default_lbl) + else: + e2_match_patch_jmp(buf, jmp_pos, default_lbl) + else: + if is_jcc != 0: + e2_big_patch_jcc(buf, jmp_pos, trap_pos) + else: + e2_big_patch_jmp(buf, jmp_pos, trap_pos) + +// Emit the balanced tree. p is the absolute start of the tree, s2 the CASE +// start, idx the sorted CASE-relative indices, n the unique value count. +fn e2_emit_match_binary(buf: string, p: int, s2: int, idx: string, n: int, default_lbl: int, tagged: int) -> int: + g_match_bin_emits = g_match_bin_emits + 1 + tree_base := p + trap_pos := tree_base + n * E2_MATCH_NODE_SZ + k : ., mut = 0 + loop: + if k >= n: + break + // Locate pre-order node k. The pre-order rank arithmetic mirrors the + // recursive layout: left child = rank+1, right child = rank+1+left_size. + lo : ., mut = 0 + hi : ., mut = n + rank : ., mut = 0 + mid : ., mut = -1 + loop: + if mid >= 0: + break + m := lo + (hi - lo) / 2 + if k == rank: + mid = m + break + left_n := m - lo + if k < rank + 1 + left_n: + hi = m + rank = rank + 1 + else: + lo = m + 1 + rank = rank + 1 + left_n + + q := tree_base + k * E2_MATCH_NODE_SZ + ci := cell_load(idx, mid * 8) + cbase := (s2 + ci) * CCR_V10_CASE_DISK + cval := cell_load(g_ccr_case, cbase + CCR_CASE_VALUE) + imm : ., mut = cval + if tagged != 0: + imm = cval * 2 + w : ., mut = q + w = w + e2_big_mov_imm64(buf, w, 11, imm) + w = w + e2_alu(buf, w, 57) // cmp r10, r11 + je_pos := w + w = w + e2_big_jcc(buf, w, 4) // je hit + jl_pos := w + w = w + e2_big_jcc(buf, w, 12) // jl lower + jmp_pos := w + w = w + e2_big_jmp(buf, w) // jmp upper + + // Hit: the case's own arm label, via the standard pending backpatch. + hit_lbl := e2_match_case_label(cell_load_i32(g_ccr_case, cbase + CCR_CASE_TARGET)) + e2_match_patch_jcc(buf, je_pos, hit_lbl) + + // Lower edge: the left subtree's root is the next pre-order node. + left_n2 := mid - lo + if left_n2 > 0: + e2_big_patch_jcc(buf, jl_pos, tree_base + (k + 1) * E2_MATCH_NODE_SZ) + else: + e2_match_patch_miss(buf, jl_pos, 1, default_lbl, trap_pos) + + // Upper edge: skip the whole left subtree. + if mid + 1 < hi: + e2_big_patch_jmp(buf, jmp_pos, tree_base + (k + 1 + left_n2) * E2_MATCH_NODE_SZ) + else: + e2_match_patch_miss(buf, jmp_pos, 0, default_lbl, trap_pos) + k = k + 1 + + if default_lbl < 0: + // No DEFAULT: a miss is a trap. The linear path has the same ud2 shape. + e2_w8(buf, trap_pos, 15) + e2_w8(buf, trap_pos + 1, 11) + return n * E2_MATCH_NODE_SZ + 2 + return n * E2_MATCH_NODE_SZ + +// ── v10 dense MATCH: rodata jump table ──────────────────────────────────── +// +// A dense integer range lowers to a rodata table of signed 32-bit offsets +// relative to the table base: +// +// bounds check (min/max miss -> DEFAULT) +// index = (scrutinee - min) >> (tagged ? 1 : 0) +// lea r11, [rip+table] +// movsxd rax, dword [r11+r10*4] +// add rax, r11 +// jmp rax +// +// The table is written during the rodata phase, after strings and before the +// HIT pool. Every slot is present for [min,max]; holes point at DEFAULT. +// Tables are only selected when there are >=8 unique values, the range is at +// most twice as wide as the value count, and a DEFAULT arm exists. Everything +// else keeps the linear/binary behavior above. + +g_match_dense_min_unique : int = 8 + +fn e2_match_default_label(s2: int, s3: int) -> int: + ci : ., mut = 0 + loop: + if ci >= s3: + break + cbase := (s2 + ci) * CCR_V10_CASE_DISK + if cell_load32(g_ccr_case, cbase + CCR_CASE_KIND) == CCR_CASE_KIND_DEFAULT: + lbl := e2_match_case_label(cell_load_i32(g_ccr_case, cbase + CCR_CASE_TARGET)) + if lbl >= 0: + return lbl + ci = ci + 1 + return -1 + +fn e2_match_case_value(s2: int, ci: int) -> int: + return cell_load(g_ccr_case, (s2 + ci) * CCR_V10_CASE_DISK + CCR_CASE_VALUE) + +// Return the table width (max-min+1) when the dense predicate holds, else 0. +// This is the single predicate shared by the plan channel, the emitter, and the +// rodata table writer. All target labels must resolve: the table stores absolute +// positions, so a missing label would desync code and data. +fn e2_match_table_span(s2: int, s3: int) -> int: + if s3 <= 0: + return 0 + if e2_match_default_label(s2, s3) < 0: + return 0 + idx := alloc((s3 + 1) * 8) + n := e2_match_sort_cases(s2, s3, idx) + if n < g_match_dense_min_unique: + return 0 + vi : ., mut = 0 + loop: + if vi >= n: + break + vci := cell_load(idx, vi * 8) + vt := cell_load_i32(g_ccr_case, (s2 + vci) * CCR_V10_CASE_DISK + CCR_CASE_TARGET) + if e2_match_case_label(vt) < 0: + return 0 + vi = vi + 1 + first := cell_load(idx, 0) + last := cell_load(idx, (n - 1) * 8) + span := e2_match_case_value(s2, last) - e2_match_case_value(s2, first) + 1 + if span <= 0: + return 0 + if span > n * 2: + return 0 + return span + +fn e2_label_abs_pos(lbl: int) -> int: + if lbl < 0: + return -1 + if lbl >= g_label_abs_cap: + return -1 + return cell_load(g_label_abs, lbl * 8) + +// Emit the dense dispatch. span comes from e2_match_table_span; the caller has +// already loaded r10 (and tagged it for enum/tagged-int) exactly as the other +// MATCH paths do. +fn e2_emit_match_table(buf: string, p: int, s2: int, s3: int, span: int, tagged: int) -> int: + g_match_table_emits = g_match_table_emits + 1 + idx := alloc((s3 + 1) * 8) + n := e2_match_sort_cases(s2, s3, idx) + first := cell_load(idx, 0) + last := cell_load(idx, (n - 1) * 8) + minv := e2_match_case_value(s2, first) + maxv := e2_match_case_value(s2, last) + default_lbl := e2_match_default_label(s2, s3) + min_imm : ., mut = minv + max_imm : ., mut = maxv + if tagged != 0: + min_imm = minv * 2 + max_imm = maxv * 2 + + w : ., mut = p + w = w + e2_big_mov_imm64(buf, w, 11, min_imm) + w = w + e2_alu(buf, w, 57) // cmp r10, r11 + lo_pos := w + w = w + e2_big_jcc(buf, w, 12) // jl default + w = w + e2_big_mov_imm64(buf, w, 11, max_imm) + w = w + e2_alu(buf, w, 57) // cmp r10, r11 + hi_pos := w + w = w + e2_big_jcc(buf, w, 15) // jg default + w = w + e2_big_mov_imm64(buf, w, 11, min_imm) + w = w + e2_alu(buf, w, 41) // sub r10, r11 + if tagged != 0: + w = w + e2_big_untag_reg(buf, w, 10) // sar r10, 1 + + // Table lives after the strings, whose final size is not known during code + // emission (Phase 3 may still intern BigInt decimal strings). Record the LEA + // displacement position with this table's ordinal; the rodata writer patches + // it once the table has an actual address. + table_id := g_match_table_cursor + grow_match_table_ref(g_match_table_ref_count + 1) + cell_store(g_match_table_ref_pos, g_match_table_ref_count * 8, w) + cell_store(g_match_table_ref_id, g_match_table_ref_count * 8, table_id) + g_match_table_ref_count = g_match_table_ref_count + 1 + w = w + e2_lrb(buf, w, 0) // lea r11, [rip+table] + + // movsxd rax, dword [r11+r10*4] + w = w + emit_rex(buf, w, 1, 0, 1, 1) + e2_w8(buf, w, 99) + w = w + 1 + w = w + emit_modrm(buf, w, 0, 0, 4) + w = w + emit_sib(buf, w, 2, 2, 3) + // add rax, r11 + w = w + emit_rex(buf, w, 1, 1, 0, 0) + e2_w8(buf, w, 1) + w = w + 1 + w = w + emit_modrm(buf, w, 3, 3, 0) + // jmp rax + e2_w8(buf, w, 255) + w = w + 1 + w = w + emit_modrm(buf, w, 3, 4, 0) + + // A table is only chosen with a DEFAULT arm, so both miss edges land there. + e2_match_patch_miss(buf, lo_pos, 1, default_lbl, w) + e2_match_patch_miss(buf, hi_pos, 1, default_lbl, w) + g_match_table_cursor = table_id + 1 + return w - p + +// Write dense tables into rodata at ro_tables_start (absolute buffer position, +// immediately after the strings). Rescans functions/instrs in emission order, +// uses the same e2_match_table_span predicate, and patches each table's recorded +// LEA with the actual address. Returns the table bytes written. The LEA patch is +// deferred because Phase 3 may intern BigInt decimal strings after code emission, +// so no in-rodata offset is stable at e2_emit_match_table time. +fn e2_match_write_tables(buf: string, ro_tables_start: int) -> int: + g_match_table_ro_cursor = 0 + table_bytes : ., mut = 0 + fi : ., mut = 0 + loop: + if fi >= g_ir_func_count: + break + ist := cell_load(g_ir_func_instr_start, fi * 8) + ic := cell_load(g_ir_func_instr_count, fi * 8) + ii : ., mut = 0 + loop: + if ii >= ic: + break + inst := ist + ii + if iri_op(inst) == IR_MATCH: + s2 := iri_s2(inst) + s3 := iri_s3(inst) + span := e2_match_table_span(s2, s3) + if span > 0: + idx := alloc((s3 + 1) * 8) + n := e2_match_sort_cases(s2, s3, idx) + first := cell_load(idx, 0) + minv := e2_match_case_value(s2, first) + default_lbl := e2_match_default_label(s2, s3) + table_id := g_match_table_ro_cursor + table_abs := ro_tables_start + table_bytes + def_abs := e2_label_abs_pos(default_lbl) + if def_abs < 0: + def_abs = table_abs + vi : ., mut = 0 + k : ., mut = 0 + loop: + if k >= span: + break + v := minv + k + target_abs : ., mut = def_abs + if vi < n: + vci := cell_load(idx, vi * 8) + if e2_match_case_value(s2, vci) == v: + vt := cell_load_i32(g_ccr_case, (s2 + vci) * CCR_V10_CASE_DISK + CCR_CASE_TARGET) + ta := e2_label_abs_pos(e2_match_case_label(vt)) + if ta >= 0: + target_abs = ta + vi = vi + 1 + w32(buf, table_abs + k * 4, target_abs - table_abs) + k = k + 1 + // Patch every LEA that refers to this table ordinal. + ri : ., mut = 0 + loop: + if ri >= g_match_table_ref_count: + break + if cell_load(g_match_table_ref_id, ri * 8) == table_id: + ref_pos := cell_load(g_match_table_ref_pos, ri * 8) + w32(buf, ref_pos + 3, table_abs - (ref_pos + 7)) + ri = ri + 1 + g_match_table_ro_cursor = table_id + 1 + table_bytes = table_bytes + span * 4 + ii = ii + 1 + fi = fi + 1 + return table_bytes + fn emit_instr(instr_idx: int, buf: string, pos: int) -> int: op := iri_op(instr_idx) d := iri_dest(instr_idx) @@ -1734,6 +2159,11 @@ fn emit_instr(instr_idx: int, buf: string, pos: int) -> int: if li >= 0: grow_label_poses(li + 1) cell_store(g_label_poses, li * 8, pos) + // Global map: label numbers are process-global and the rodata table + // writer runs after all functions, when g_label_poses only holds the + // last function's labels. + grow_label_abs(li + 1) + cell_store(g_label_abs, li * 8, pos) if li + 1 > g_label_count: g_label_count = li + 1 // Patch all pending forward jumps targeting this label @@ -1748,6 +2178,147 @@ fn emit_instr(instr_idx: int, buf: string, pos: int) -> int: pi = pi + 1 return 0 + if op == IR_MATCH: + // v10 multi-way dispatch. s1 = scrutinee var, s2 = CASE start, + // s3 = CASE count. Every target is a NOD id whose node is IR_LABEL; the + // label number drives backpatching. <=2 value rows keep the linear + // cmp/je chain; larger sparse ranges use the balanced tree above. + ti_s := irv_type(s1) + cp = cp + e2_load_var(buf, pos+cp, 10, s1) + + // Enum CASE rows compare the tag word, not the pointer. A match cannot + // mix enum and value patterns under the type checker, so the first + // non-default row selects the scrutinee form. + first_kind : ., mut = -1 + fk : ., mut = 0 + loop: + if fk >= s3: + break + fk_kind := cell_load32(g_ccr_case, (s2 + fk) * CCR_V10_CASE_DISK + CCR_CASE_KIND) + if fk_kind != CCR_CASE_KIND_DEFAULT: + first_kind = fk_kind + break + fk = fk + 1 + if first_kind == CCR_CASE_KIND_ENUM: + // mov r10, [r10 + 0]: the enum tag word. + cp = cp + emit_rex(buf, pos+cp, 1, 10/8, 0, 10/8) + e2_w8(buf, pos+cp, 139) + cp = cp + 1 + cp = cp + emit_modrm(buf, pos+cp, 2, 10%8, 10%8) + cp = cp + e2_w32(buf, pos+cp, 0) + if g_x86_big_enabled != 0: + cp = cp + e2_big_tag_reg(buf, pos+cp, 10) + + // Pre-scan: locate DEFAULT. It emits nothing, so the linear emission + // sequence below stays byte-identical for <=2 value rows. + default_lbl : ., mut = -1 + ci : ., mut = 0 + loop: + if ci >= s3: + break + cbase := (s2 + ci) * CCR_V10_CASE_DISK + ckind := cell_load32(g_ccr_case, cbase + CCR_CASE_KIND) + if ckind == CCR_CASE_KIND_DEFAULT: + ctarget := cell_load_i32(g_ccr_case, cbase + CCR_CASE_TARGET) + if ctarget >= 0 && ctarget < g_ir_instr_count: + if iri_op(ctarget) == IR_LABEL: + default_lbl = iri_s1(ctarget) + ci = ci + 1 + + // Dense dispatch: >=8 unique values in a range at most twice as wide + // as the value count, with a DEFAULT arm, use a rodata jump table. + // Sparse (>2 rows, not dense) keeps the balanced binary search tree. + // Both predicates are shared with --dump-match-plan. + table_span := e2_match_table_span(s2, s3) + if table_span > 0: + tab_tagged : ., mut = 0 + if g_x86_big_enabled != 0: + if first_kind == CCR_CASE_KIND_ENUM || ti_s == TI_INT: + tab_tagged = 1 + cp = cp + e2_emit_match_table(buf, pos+cp, s2, s3, table_span, tab_tagged) + return cp + + if e2_match_use_binary(s2, s3) != 0: + idx := alloc((s3 + 1) * 8) + uniq := e2_match_sort_cases(s2, s3, idx) + labels_ok : ., mut = 1 + vi : ., mut = 0 + loop: + if vi >= uniq: + break + vci := cell_load(idx, vi * 8) + vtarget := cell_load_i32(g_ccr_case, (s2 + vci) * CCR_V10_CASE_DISK + CCR_CASE_TARGET) + if e2_match_case_label(vtarget) < 0: + labels_ok = 0 + vi = vi + 1 + if labels_ok != 0: + bin_tagged : ., mut = 0 + if g_x86_big_enabled != 0: + if first_kind == CCR_CASE_KIND_ENUM || ti_s == TI_INT: + bin_tagged = 1 + cp = cp + e2_emit_match_binary(buf, pos+cp, s2, idx, uniq, default_lbl, bin_tagged) + return cp + + ci = 0 + loop: + if ci >= s3: + break + cbase := (s2 + ci) * CCR_V10_CASE_DISK + ckind := cell_load32(g_ccr_case, cbase + CCR_CASE_KIND) + ctarget := cell_load_i32(g_ccr_case, cbase + CCR_CASE_TARGET) + if ckind == CCR_CASE_KIND_DEFAULT: + if ctarget >= 0 && ctarget < g_ir_instr_count: + if iri_op(ctarget) == IR_LABEL: + default_lbl = iri_s1(ctarget) + else: + lbl_no : ., mut = -1 + if ctarget >= 0 && ctarget < g_ir_instr_count: + if iri_op(ctarget) == IR_LABEL: + lbl_no = iri_s1(ctarget) + if lbl_no >= 0: + cval := cell_load(g_ccr_case, cbase + CCR_CASE_VALUE) + // Tagged int form: enum tags are ints in user code, and an + // int scrutinee slot is tagged. In raw-int mode neither is, + // so the enum tag must stay raw too. + tagged : ., mut = 0 + if g_x86_big_enabled != 0: + if ckind == CCR_CASE_KIND_ENUM || ti_s == TI_INT: + tagged = 1 + imm := cval + if tagged != 0: + imm = cval * 2 + cp = cp + e2_big_mov_imm64(buf, pos+cp, 11, imm) + cp = cp + e2_alu(buf, pos+cp, 57) // cmp r10, r11 + jp := pos + cp + cp = cp + e2_big_jcc(buf, pos+cp, 4) // je rel32 + if cell_load(g_label_poses, lbl_no * 8) >= 0: + e2_big_patch_jcc(buf, jp, cell_load(g_label_poses, lbl_no * 8)) + else: + grow_pending(g_pending_count + 1) + cell_store(g_pending_pos, g_pending_count*8, jp + 2) + cell_store(g_pending_label, g_pending_count*8, lbl_no) + g_pending_count = g_pending_count + 1 + ci = ci + 1 + + // No value hit -> default. A range without DEFAULT is a trap: the + // design forbids falling through into the first arm body. + if default_lbl >= 0: + jmp_pos := pos + cp + cp = cp + e2_jmp(buf, pos+cp, 0) + if cell_load(g_label_poses, default_lbl * 8) >= 0: + target := cell_load(g_label_poses, default_lbl * 8) + e2_w32(buf, jmp_pos + 1, target - (jmp_pos + 5)) + else: + grow_pending(g_pending_count + 1) + cell_store(g_pending_pos, g_pending_count*8, jmp_pos + 1) + cell_store(g_pending_label, g_pending_count*8, default_lbl) + g_pending_count = g_pending_count + 1 + else: + e2_w8(buf, pos+cp, 15) + e2_w8(buf, pos+cp+1, 11) // ud2: invalid opcode trap + cp = cp + 2 + return cp + if op == IR_LOAD_ENUM_TAG && d >= 0: do2 := g2_slot(d) // Read the tag through e2_load_var: locals use [rbp+slot], globals use a diff --git a/src/arch/x86_64/regalloc.cr b/src/arch/x86_64/regalloc.cr index 80566438..ed9d0136 100644 --- a/src/arch/x86_64/regalloc.cr +++ b/src/arch/x86_64/regalloc.cr @@ -580,7 +580,8 @@ fn alloc_registers(): if cell_load(st, lvd * 8) == 0: // dest 定值同指令自读(s1/s2 == d):读先于写,该读 // 可能取上一轮值,按读首引用(携带)处理 - if s1 == d || s2 == d: + // MATCH 的 s2 是 CASE 起始下标,不是变量。 + if s1 == d || (op != IR_MATCH && s2 == d): cell_store(st, lvd * 8, 2) cell_store(car, lvd * 8, 1) else: @@ -637,6 +638,28 @@ fn alloc_registers(): if t3 > pp && t3 <= b0: cell_store(skp, (pp + 1 - t0) * 8, cell_load(skp, (pp + 1 - t0) * 8) + 1) cell_store(skp, (t3 - t0) * 8, cell_load(skp, (t3 - t0) * 8) - 1) + else if iri_op(ist + pp) == IR_MATCH: + // Every CASE target is a conditional successor. Mark + // each forward arm entry as skippable, exactly like a + // BRANCH target. Over-marking only lowers register + // pressure, so duplicate targets are harmless. + mc := iri_s3(ist + pp) + ms := iri_s2(ist + pp) + mj : ., mut = 0 + loop: + if mj >= mc: + break + mtgt := cell_load_i32(g_ccr_case, (ms + mj) * CCR_V10_CASE_DISK + CCR_CASE_TARGET) + mlab : ., mut = -1 + if mtgt >= 0 && mtgt < g_ir_instr_count: + if iri_op(mtgt) == IR_LABEL: + mlab = iri_s1(mtgt) + if mlab >= 0: + mt := label_pos_of(mlab, lab_id, lab_ps, ln) + if mt > pp && mt <= b0: + cell_store(skp, (pp + 1 - t0) * 8, cell_load(skp, (pp + 1 - t0) * 8) + 1) + cell_store(skp, (mt - t0) * 8, cell_load(skp, (mt - t0) * 8) - 1) + mj = mj + 1 else if iri_op(ist + pp) == IR_JUMP: t1 := label_pos_of(iri_s1(ist + pp), lab_id, lab_ps, ln) if t1 > pp && t1 <= b0: diff --git a/src/ci/run.sh b/src/ci/run.sh index 8b6c166e..d3617151 100755 --- a/src/ci/run.sh +++ b/src/ci/run.sh @@ -136,6 +136,7 @@ case "$CI_JOB_NAME" in python3 tests/selfhost/test_purity.py # 效应/纯度批判据(真纯度 13 例 + 链语义 4 例的自测通道驱动) python3 tests/selfhost/test_ccr_v10_format.py # v10 wire oracle + header/SYM/NOD negative fixtures(Task 1 + final-review Critical 1/2/6) python3 tests/selfhost/test_ccr_v10_stream.py # v10 canonical forward-only loader structure + truncation/order negative fixtures + python3 tests/selfhost/test_multibranch_v10.py # v10 MATCH opcode + CASE segment 24B layout/loader domain fixtures python3 tests/selfhost/test_ccr_v7.py # 效应/纯度批 Task 3:.ccr 层三条边集语义断言(取代「与旧版逐字节同」) python3 tests/selfhost/test_ccr_types.py # R2 P4 Task 1/2/3/4 + P5 Task 2:段机制(版本 8 + loader 三闸/必备集 + 旧 v7 拒收)8 例 + TYPE 内容面(行表/项 DAG 序列化 + 确定性装填 + corearch 读回对拍 + loader 负分支)10 例 + IFACE 内容面(五小节/条目扩列 16/形状命名化/签名项化/impl 边与方法表/corearch 读回/跨段引用域/确定性)10 例 + DFNode.TK 项槽(逐节点规则 + dex 语料 + NOD 36B/同序 + 冷/暖快照对称 + dump 零产物影响 + .cir 布局未变)6 例 = 34 例 + P5 T2 单槽化 6 例(复合行走辅码 + 互斥不变量 + F1 hotpatch 无面 + 复合行冷/暖对称 + 复合行 NOD 同序 + 快照盘面派生码保真)= 40 例 + R2 P6 Task 3 项索引 8 例(版本闸 v8 拒收 / NOD 40B 字段序 / 项索引落盘读回对拍(非退化)/ 两通道面一致 / 辅码面与复合行保真 / 项索引越界与不一致 ⇒ 拒绝 / 段界检 / D18 纯度静态)= 49 例(**+1 他批新增,本批机械核对同步**:实测 49/49) python3 tests/selfhost/test_impl.py diff --git a/src/compiler/ast.cr b/src/compiler/ast.cr index 53884848..a87a4ec8 100644 --- a/src/compiler/ast.cr +++ b/src/compiler/ast.cr @@ -243,7 +243,7 @@ EXPR_AT : int = 46 // @builtin: a=name_ni, b=args_node, c=0, iv=0, t // Desugared constructs EXPR_TRY : int = 33 // a=expr being tried (? operator) EXPR_UNSAFE : int = 34 // a=block body -EXPR_STRUCTPAT : int = 35 // struct pattern: a=name ni, b=first wrapper(连续;wrapper kind=EXPR_NONE 且 a=子模式节点), c=field count +EXPR_STRUCTPAT : int = 35 // struct pattern: a=name ni, b=first wrapper(连续;wrapper kind=EXPR_NONE,a=子模式节点,b=字段名 ni), c=field count EXPR_AS : int = 36 // a=expr, b=type node (cast: expr as Type) EXPR_PTRTYPE : int = 47 // a=inner_type (for *T in type position) EXPR_EXTERN : int = 48 // a=name_ni, b=first_param, c=param_count, data=ffi_lang_ni @@ -257,6 +257,7 @@ EXPR_OPTIONAL : int = 49 // a=inner type node (T? 类型位置) // hole for diagnostics. EXPR_INTERP_HOLE : int = 50 // data=hole text ni(解析期);checker 改写后本 kind 不再存在 EXPR_BIGINT : int = 51 // a=canonical decimal string ni; type TY_INT +EXPR_TUPLEPAT : int = 52 // tuple pattern: a=first wrapper(连续;wrapper kind=EXPR_NONE,a=子模式节点,b=元素下标), b=element count // Field representation in struct literal: two consecutive AST nodes // (name_idx, value_idx, line=line, col=col) @@ -645,6 +646,7 @@ IR_F2I : int = 50 // dest=int_var, s1=dex_var(bits); binary64 to int(cvttsd IR_APPROX : int = 51 //无操作数注解:apx 变量声明处(语义许可,后端可忽略,interp 跳过) IR_PTR_OF : int = 52 // dest=same int var, s1=same string var; view/convert address to mathematical int IR_STR_OF : int = 53 // dest=same string var, s1=same int var; view/convert mathematical int to raw address +IR_MATCH : int = 54 // src1=scrutinee var, src2=CASE table start, src3=case count; multi-way dispatch // Resolution flag for BRANCH/JUMP (stored in type_kind field after label resolution) IR_RESOLVED : int = 1 diff --git a/src/compiler/ccr_io.cr b/src/compiler/ccr_io.cr index 5d3c9a58..3b427381 100644 --- a/src/compiler/ccr_io.cr +++ b/src/compiler/ccr_io.cr @@ -31,6 +31,11 @@ // type_id i32, flags u32, first_edge u32, edge_count u32}]. // src2/src3 never carry width, byte offset, stride, or alignment. // EDG(8): [count][8B {to_nod u32, kind u32}]. +// CASE(9): [count][24B {value i64, kind u32, str_index i32, target_nod i32, +// flags u32}]. kind is 0=value, 1=enum, 2=string, 3=default. target_nod is +// a NOD id whose node is IR_LABEL. str_index is -1 or a STR index, and a +// string case needs a real index. flags is reserved and zero in v10. CASE is +// last so every target and STR reference is already visible when it loads. // ENT(6): [count][24B {var_id i32, version u32, def_nod i32, live_start u32, // live_end u32, flags u32}]. home is not part of v10. // REG(7): [count][24B {kind u32, parent i32, enter_nod u32, exit_nod u32, @@ -340,8 +345,76 @@ fn ccr_reg_seg_size() -> int: return 4 + g_sg_count * CCR_V10_REG_DISK fn ccr_case_seg_size() -> int: - // CASE is an empty table until match lowering writes it: 4B case_count = 0. - return 4 + // CASE is 4B case_count plus 24B records. Match lowering fills g_ccr_case; + // before that the count is zero and the segment is a bare 4B zero. + return 4 + g_ccr_case_count * CCR_V10_CASE_DISK + +// Write-side CASE domain check. The in-memory table is the source of truth, so +// a malformed row must reject the save instead of producing a file the loader +// would refuse. Targets are NOD ids of LABEL nodes; the value domain is checked +// by the projection that consumes it, not here. +fn ccr_validate_cases() -> int: + if g_ccr_case_count < 0: + return 0 + ci : ., mut = 0 + loop: + if ci >= g_ccr_case_count: + break + base := ci * CCR_V10_CASE_DISK + kind := cell_load32(g_ccr_case, base + CCR_CASE_KIND) + sidx := cell_load_i32(g_ccr_case, base + CCR_CASE_STR) + target := cell_load_i32(g_ccr_case, base + CCR_CASE_TARGET) + cflags := cell_load32(g_ccr_case, base + CCR_CASE_FLAGS) + if kind < CCR_CASE_KIND_VALUE || kind > CCR_CASE_KIND_DEFAULT: + return 0 + if sidx < -1 || sidx >= g_str_count: + return 0 + if kind == CCR_CASE_KIND_STRING && sidx < 0: + return 0 + if target < 0 || target >= g_ir_instr_count: + return 0 + if iri_op(target) != IR_LABEL: + return 0 + if cflags != 0: + return 0 + ci = ci + 1 + // Every MATCH owns a contiguous CASE range with at most one DEFAULT. + // Multiple matches in one program therefore have one default each; the + // table itself is global. + mi : ., mut = 0 + loop: + if mi >= g_ir_instr_count: + break + if iri_op(mi) == IR_MATCH: + mstart := iri_s2(mi) + mcount := iri_s3(mi) + if mstart < 0 || mcount < 0: + return 0 + if mstart + mcount > g_ccr_case_count: + return 0 + mdef : ., mut = 0 + menum : ., mut = 0 + mvalue : ., mut = 0 + mj : ., mut = 0 + loop: + if mj >= mcount: + break + mk := cell_load32(g_ccr_case, (mstart + mj) * CCR_V10_CASE_DISK + CCR_CASE_KIND) + if mk == CCR_CASE_KIND_DEFAULT: + mdef = mdef + 1 + if mdef > 1: + return 0 + else if mk == CCR_CASE_KIND_ENUM: + menum = menum + 1 + else if mk == CCR_CASE_KIND_VALUE: + mvalue = mvalue + 1 + mj = mj + 1 + if menum > 0 && mvalue > 0: + // The backend selects one scrutinee form (enum tag vs value + // slot) for the whole range. + return 0 + mi = mi + 1 + return 1 // ── Header flag classification (spec §3.1) ────────────────────────────── // SYSTEM boundary ops: runtime/FFI/ABI surfaces. ARENA_NEW only counts when the @@ -388,12 +461,12 @@ fn ccr_op_is_concurrent(op: int) -> int: return 1 return 0 -// v10 opcode table. The IR opcode values are assigned densely 0..53 in ast.cr; +// v10 opcode table. The IR opcode values are assigned densely 0..54 in ast.cr; // anything outside that table is not a v10 operation. Rejecting unknown opcodes // before emission also prevents emit_instr from silently falling through as a // zero-byte instruction. fn ccr_op_is_known(op: int) -> int: - if op >= IR_NOP && op <= IR_STR_OF: + if op >= IR_NOP && op <= IR_MATCH: return 1 return 0 @@ -527,6 +600,15 @@ fn ccr_validate_nod_operands() -> int: return -1 if ccr_label_known(ids, ln, s2) == 0 || ccr_label_known(ids, ln, s3) == 0: return -1 + else if op == IR_MATCH: + // Minimal v10 MATCH shape: a real scrutinee var plus a non-negative + // CASE range. -1 is the reserved "none" id and is not a scrutinee. + // The range is checked against the CASE table after that segment + // loads (match projection work lands with the backend). + if s1 < 0 || ccr_nod_var_ok(s1) == 0: + return -1 + if s2 < 0 || s3 < 0: + return -1 else if op == IR_JUMP || op == IR_LABEL: if ccr_label_known(ids, ln, s1) == 0: return -1 @@ -592,7 +674,7 @@ fn ccr_validate_nod_operands() -> int: // fact. ALLOC_ARRAY/ADDR_INDEX/DEREF/STORE_PTR are handled explicitly because // their old src2/src3 facts are the migration's critical cases. fn ccr_nod_src3_semantic(op: int) -> int: - if op == IR_UNARY || op == IR_BINARY || op == IR_CALL || op == IR_ALLOC_STRUCT || op == IR_LOAD_FIELD || op == IR_STORE_FIELD || op == IR_STORE_INDEX || op == IR_BRANCH || op == IR_SLICE || op == IR_SPAWN || op == IR_HOTPATCH_ROUTE || op == IR_CALL_EXTERN || op == IR_LOAD_INDEX: + if op == IR_UNARY || op == IR_BINARY || op == IR_CALL || op == IR_ALLOC_STRUCT || op == IR_LOAD_FIELD || op == IR_STORE_FIELD || op == IR_STORE_INDEX || op == IR_BRANCH || op == IR_MATCH || op == IR_SLICE || op == IR_SPAWN || op == IR_HOTPATCH_ROUTE || op == IR_CALL_EXTERN || op == IR_LOAD_INDEX: return 1 return 0 @@ -812,6 +894,10 @@ fn save_ccr(path: string) -> int: // lossy file instead of letting w32 keep only the low bits. if ccr_validate_i32_fields() == 0: return -1 + // CASE rows are in-memory 24B records. Reject a malformed table here so the + // writer never emits a file the loader would reject. + if ccr_validate_cases() == 0: + return -1 // ENT data is computed first. The corec writer calls compute_live_ranges(), // which calls compute_entries per function and fills the in-memory 24B @@ -1325,10 +1411,26 @@ fn save_ccr(path: string) -> int: pos = pos + 4 // kind ei3 = ei3 + 1 - // === CASE: empty table for now (4B case_count = 0). CASE is last because it - // may reference every NOD target once match lowering fills it in. - buf_write_u32(buf, pos, 0) + // === CASE: 24B records from g_ccr_case. CASE is last because targets may + // reference every NOD label and string cases may reference every STR entry. + buf_write_u32(buf, pos, g_ccr_case_count) pos = pos + 4 + ci3 : ., mut = 0 + loop: + if ci3 >= g_ccr_case_count: + break + cbase := ci3 * CCR_V10_CASE_DISK + buf_write_i64(buf, pos, cell_load(g_ccr_case, cbase + CCR_CASE_VALUE)) + pos = pos + 8 + buf_write_u32(buf, pos, cell_load32(g_ccr_case, cbase + CCR_CASE_KIND)) + pos = pos + 4 + buf_write_i32(buf, pos, cell_load_i32(g_ccr_case, cbase + CCR_CASE_STR)) + pos = pos + 4 + buf_write_i32(buf, pos, cell_load_i32(g_ccr_case, cbase + CCR_CASE_TARGET)) + pos = pos + 4 + buf_write_u32(buf, pos, cell_load32(g_ccr_case, cbase + CCR_CASE_FLAGS)) + pos = pos + 4 + ci3 = ci3 + 1 // The size calculator and the writer are two independent walks. A mismatch // means a section changed without updating calc_ccr_size; refuse to write a @@ -1565,6 +1667,8 @@ fn load_ccr(data: string, fsize: int) -> int: g_entry_count = 0 g_v7_edge_count = 0 g_v7_nod_count = 0 // NOD 对象缓冲计数(NOD 段载入后置位) + g_ccr_case = "" + g_ccr_case_count = 0 // TYPE 段有真实内存表(g_types 记录表 + 项层 g_type_terms/ // g_tt_index),复位在 TYPE 段解析处(记录表大小随段内容,且需与项层 memo // 同步作废;见该段注释)。 @@ -2623,17 +2727,92 @@ fn load_ccr(data: string, fsize: int) -> int: return -1 // 2 行走完 == 段体大小 - // === CASE: empty table for now === - // The loader requires case_count == 0 until match lowering writes CASE. Any - // other count is an unknown shape, so reject instead of treating it as empty. + // === CASE: [case_count][24B records] === + // Targets are NOD ids of LABEL nodes; CASE loads after NOD, so the whole + // graph is visible. Records are bounds-checked before each read and the + // walk must end exactly at the segment boundary. if !ccr_has_bytes(pos, 4, seg_end9): return -1 case_cnt := buf_read_u32(data, pos) pos = pos + 4 - if case_cnt != 0: + if case_cnt > (seg_end9 - pos) / CCR_V10_CASE_DISK: return -1 + case_buf : string, mut = "" + if case_cnt > 0: + case_buf = alloc(case_cnt * CCR_V10_CASE_DISK) + ci4 : ., mut = 0 + loop: + if ci4 >= case_cnt: + break + if !ccr_has_bytes(pos, CCR_V10_CASE_DISK, seg_end9): + return -1 + cbase := ci4 * CCR_V10_CASE_DISK + cval := buf_read_i64(data, pos) + pos = pos + 8 + ckind := buf_read_u32(data, pos) + pos = pos + 4 + csidx := buf_read_i32(data, pos) + pos = pos + 4 + ctarget := buf_read_i32(data, pos) + pos = pos + 4 + cflags := buf_read_u32(data, pos) + pos = pos + 4 + if ckind < CCR_CASE_KIND_VALUE || ckind > CCR_CASE_KIND_DEFAULT: + return -1 + if csidx < -1 || csidx >= g_str_count: + return -1 + if ckind == CCR_CASE_KIND_STRING && csidx < 0: + return -1 + if ctarget < 0 || ctarget >= g_v7_nod_count: + return -1 + if nod_op(ctarget) != IR_LABEL: + return -1 + // v10 defines no CASE flag bits yet; reject unknown meaning. + if cflags != 0: + return -1 + cell_store(case_buf, cbase + CCR_CASE_VALUE, cval) + cell_store32(case_buf, cbase + CCR_CASE_KIND, ckind) + cell_store32(case_buf, cbase + CCR_CASE_STR, csidx) + cell_store32(case_buf, cbase + CCR_CASE_TARGET, ctarget) + cell_store32(case_buf, cbase + CCR_CASE_FLAGS, cflags) + ci4 = ci4 + 1 if pos != seg_end9: return -1 + // MATCH ranges are slices of the global table. Each range must fit and may + // contain at most one DEFAULT; different matches may each have one. + mi4 : ., mut = 0 + loop: + if mi4 >= g_v7_nod_count: + break + if nod_op(mi4) == IR_MATCH: + mstart4 := nod_s2(mi4) + mcount4 := nod_s3(mi4) + if mstart4 < 0 || mcount4 < 0: + return -1 + if mstart4 + mcount4 > case_cnt: + return -1 + mdef4 : ., mut = 0 + menum4 : ., mut = 0 + mvalue4 : ., mut = 0 + mj4 : ., mut = 0 + loop: + if mj4 >= mcount4: + break + mk4 := cell_load32(case_buf, (mstart4 + mj4) * CCR_V10_CASE_DISK + CCR_CASE_KIND) + if mk4 == CCR_CASE_KIND_DEFAULT: + mdef4 = mdef4 + 1 + if mdef4 > 1: + return -1 + else if mk4 == CCR_CASE_KIND_ENUM: + menum4 = menum4 + 1 + else if mk4 == CCR_CASE_KIND_VALUE: + mvalue4 = mvalue4 + 1 + mj4 = mj4 + 1 + if menum4 > 0 && mvalue4 > 0: + return -1 + mi4 = mi4 + 1 + g_ccr_case = case_buf + g_ccr_case_count = case_cnt // === NOD type_id domain check === // TYPE is consumed before NOD, so the row domain is already rebuilt. The diff --git a/src/compiler/ccr_lower.cr b/src/compiler/ccr_lower.cr index cbb0bd2b..175058c9 100644 --- a/src/compiler/ccr_lower.cr +++ b/src/compiler/ccr_lower.cr @@ -1,6 +1,35 @@ // === ccr_lower.cr === // HDFG linear schedule lowering. This is the only frontend step that turns -// graph nodes into the linear instruction stream stored in .ccr. +// graph nodes into the linear instruction stream stored in .ccr, and the only +// producer of the wire CASE table. + +// ── wire CASE helpers ──────────────────────────────────────────────────── +// The wire table is a flat 24B-row buffer. ccr_lower derives it from the +// semantic model (g_lat_case); rows keep the wire layout named by CCR_CASE_*. +fn grow_ccr_case(needed: int): + if needed < g_ccr_case_cap: + return + nc : ., mut = g_ccr_case_cap * 2 + if nc < 16: + nc = 16 + if nc < needed: + nc = needed + 16 + nb := alloc(nc * CCR_V10_CASE_DISK) + cell_copy(g_ccr_case, g_ccr_case_cap * CCR_V10_CASE_DISK, nb) + g_ccr_case = nb + g_ccr_case_cap = nc + +fn ccr_case_append(value: int, kind: int, str_index: int, target: int, flags: int) -> int: + idx := g_ccr_case_count + grow_ccr_case(idx + 1) + base := idx * CCR_V10_CASE_DISK + cell_store(g_ccr_case, base + CCR_CASE_VALUE, value) + cell_store32(g_ccr_case, base + CCR_CASE_KIND, kind) + cell_store32(g_ccr_case, base + CCR_CASE_STR, str_index) + cell_store32(g_ccr_case, base + CCR_CASE_TARGET, target) + cell_store32(g_ccr_case, base + CCR_CASE_FLAGS, flags) + g_ccr_case_count = idx + 1 + return idx fn lower_to_ccr(): // The graph was built in parallel with linear IR during emit(). @@ -41,6 +70,92 @@ fn lower_to_ccr(): cell_load(g_df_func_node_count, fi * 8)) fi = fi + 1 + // Derive the wire CASE table from the semantic model. The model stores body + // label numbers; resolve each target against the IR_LABEL nodes of the same + // function. Matches are visited in instruction order, so the wire rows keep + // the model's reservation order. An unresolved label stays -1 and the + // .ccr loader/saver rejects the malformed wire row. Only this function + // writes g_ccr_case. + g_ccr_case_count = 0 + + fi = 0 + loop: + if fi >= g_ir_func_count: + break + f_nstart := cell_load(g_df_func_node_start, fi * 8) + f_ncount := cell_load(g_df_func_node_count, fi * 8) + f_nend := f_nstart + f_ncount + + lbl_n : ., mut = 0 + ni = f_nstart + loop: + if ni >= f_nend: + break + if cell_load(g_df_nodes, ni * ESZ_DFNODE + OFF_DF_OPCODE) == IR_LABEL: + lbl_n = lbl_n + 1 + ni = ni + 1 + + lbl_id : string, mut = "" + lbl_nod : string, mut = "" + if lbl_n > 0: + lbl_id = alloc(lbl_n * 8) + lbl_nod = alloc(lbl_n * 8) + li : ., mut = 0 + ni = f_nstart + loop: + if ni >= f_nend: + break + if cell_load(g_df_nodes, ni * ESZ_DFNODE + OFF_DF_OPCODE) == IR_LABEL: + cell_store(lbl_id, li * 8, + cell_load(g_df_nodes, ni * ESZ_DFNODE + OFF_DF_S1)) + cell_store(lbl_nod, li * 8, ni) + li = li + 1 + ni = ni + 1 + + istart := cell_load(g_ir_func_instr_start, fi * 8) + icount := cell_load(g_ir_func_instr_count, fi * 8) + ii : ., mut = 0 + loop: + if ii >= icount: + break + inst := istart + ii + if iri_op(inst) == IR_MATCH: + ms := iri_s2(inst) + mn := iri_s3(inst) + if ms >= 0 && mn >= 0 && ms + mn <= g_lat_case_count: + wire_start := g_ccr_case_count + rj : ., mut = 0 + loop: + if rj >= mn: + break + cbase := (ms + rj) * CCR_V10_CASE_DISK + cval := cell_load(g_lat_case, cbase + CCR_CASE_VALUE) + ckind := cell_load32(g_lat_case, cbase + CCR_CASE_KIND) + csidx := cell_load_i32(g_lat_case, cbase + CCR_CASE_STR) + cflags := cell_load32(g_lat_case, cbase + CCR_CASE_FLAGS) + clabel := cell_load_i32(g_lat_case, cbase + CCR_CASE_TARGET) + cnod : ., mut = -1 + if clabel >= 0: + lj : ., mut = 0 + loop: + if lj >= lbl_n: + break + if cell_load(lbl_id, lj * 8) == clabel: + cnod = cell_load(lbl_nod, lj * 8) + break + lj = lj + 1 + ccr_case_append(cval, ckind, csidx, cnod, cflags) + rj = rj + 1 + // IR_MATCH s2/s3 become the derived wire range. The NOD + // mirror is rebuilt into g_ir_instrs above, but downstream + // readers (interp) also consume it, so keep both in sync. + iri_set_s2(inst, wire_start) + iri_set_s3(inst, mn) + cell_store(g_df_nodes, inst * ESZ_DFNODE + OFF_DF_S2, wire_start) + cell_store(g_df_nodes, inst * ESZ_DFNODE + OFF_DF_S3, mn) + ii = ii + 1 + fi = fi + 1 + // After lowering, compute per-variable usage counts for optimization passes compute_usage_counts() diff --git a/src/compiler/ccr_schema.cr b/src/compiler/ccr_schema.cr index 399142a5..6fa651a2 100644 --- a/src/compiler/ccr_schema.cr +++ b/src/compiler/ccr_schema.cr @@ -39,6 +39,23 @@ CCR_V10_REG_DISK : int = 24 CCR_V10_EDG_DISK : int = 8 CCR_V10_CASE_DISK : int = 24 +// CASE record (24B): value i64, kind u32, str index i32, target NOD id i32, +// flags u32. The target names an IR_LABEL node; CASE carries no width, byte +// extent, or target layout fact. CCR_V10_CASE_DISK is the one canonical record +// size used by size calculation, writer, and loader. +CCR_CASE_VALUE : int = 0 +CCR_CASE_KIND : int = 8 +CCR_CASE_STR : int = 12 +CCR_CASE_TARGET : int = 16 +CCR_CASE_FLAGS : int = 20 + +// CASE semantic kinds. VALUE covers int/bool/char literals, ENUM covers enum +// variant tags, STRING names a STR entry, and DEFAULT is the fall-through arm. +CCR_CASE_KIND_VALUE : int = 0 +CCR_CASE_KIND_ENUM : int = 1 +CCR_CASE_KIND_STRING : int = 2 +CCR_CASE_KIND_DEFAULT : int = 3 + // v10 header flags (spec §3.1). They describe program properties only; no bit // carries raw-int, external-binary-interface, or target identity. // bit 0 PORTABLE no raw/FFI/external-binary boundary diff --git a/src/compiler/ccr_types.cr b/src/compiler/ccr_types.cr index dfcfa6e4..8eb4aea7 100644 --- a/src/compiler/ccr_types.cr +++ b/src/compiler/ccr_types.cr @@ -440,6 +440,10 @@ fn ccr_nod_type_domain(ii: int, op: int, d: int, s1: int, s2: int, s3: int) -> i return ccr_var_type_row(d) if op == IR_LOAD_ENUM_TAG: return ccr_var_type_row(d) + if op == IR_MATCH: + // The scrutinee's semantic type is the meaningful domain; the CASE + // table carries no width or layout fact. + return ccr_var_type_row(s1) // Listed ops whose domain is genuinely unavailable from the current IR // state. They remain -1 rather than guessing. return -1 diff --git a/src/compiler/checker.cr b/src/compiler/checker.cr index 2fd36aec..8c86cf4b 100644 --- a/src/compiler/checker.cr +++ b/src/compiler/checker.cr @@ -601,7 +601,7 @@ fn loop_body_has_break(node: int) -> int: if k == EXPR_STRUCT || k == EXPR_STRUCTPAT || k == EXPR_ENUMPAT || k == EXPR_GENERIC_APPLY: // 连续子节点段:首 b、个数 c(wrapper.a = 值/子模式,经 EXPR_NONE 分支承接) return seg_has_break(ast_b(node), ast_c(node)) - if k == EXPR_ARRAY || k == EXPR_TUPLE: + if k == EXPR_ARRAY || k == EXPR_TUPLE || k == EXPR_TUPLEPAT: return seg_has_break(ast_a(node), ast_b(node)) if k == EXPR_NONE: // wrapper(a = 值节点)或基类型节点(a = 0):照 monomorph 的判据 @@ -1640,6 +1640,381 @@ fn struct_field_index_by_name(si: int, name_ni: int) -> int: j = j + 1 return -1 +// Recursively replace struct generic-parameter rows with the concrete arguments +// supplied by a TYP_GENERIC_APPLY scrutinee. Unknown/absent arguments stay as +// TYP_GENERIC_PARAM so the caller can reject/fail closed. +fn structpat_subst_ti(si: int, ti: int, scrut_ti: int) -> int: + k := get_type_kind(ti) + if k < 0: + return ti + if k == TYP_GENERIC_PARAM: + pni := get_type_data(ti) + if get_type_kind(scrut_ti) == TYP_GENERIC_APPLY: + ga_start := get_type_extra(scrut_ti) + ga_count := cell_load(g_gen_apply_data, ga_start * 8) + gpi : ., mut = 0 + loop: + if gpi >= si_generic_count(si): + break + if si_generic_name(si, gpi) == pni: + if gpi < ga_count: + return cell_load(g_gen_apply_data, (ga_start + 1 + gpi) * 8) + break + gpi = gpi + 1 + return ti + if k == TYP_ARRAY: + return alloc_type(TYP_ARRAY, structpat_subst_ti(si, get_type_data(ti), scrut_ti), get_type_extra(ti)) + if k == TYP_SLICE: + return alloc_type(TYP_SLICE, structpat_subst_ti(si, get_type_data(ti), scrut_ti), 0) + if k == TYP_PTR: + return alloc_type(TYP_PTR, structpat_subst_ti(si, get_type_data(ti), scrut_ti), get_type_extra(ti)) + if k == TYP_REF: + return alloc_type(TYP_REF, structpat_subst_ti(si, get_type_data(ti), scrut_ti), get_type_extra(ti)) + if k == TYP_OPTIONAL: + return alloc_type(TYP_OPTIONAL, structpat_subst_ti(si, get_type_data(ti), scrut_ti), 0) + if k == TYP_TUPLE: + cnt := get_type_data(ti) + st := get_type_extra(ti) + subs : string, mut + if cnt > 0: + subs = alloc(cnt * 8) + i : ., mut = 0 + loop: + if i >= cnt: + break + cell_store(subs, i * 8, structpat_subst_ti(si, cell_load(g_gen_apply_data, (st + i) * 8), scrut_ti)) + i = i + 1 + ns := g_gen_apply_data_count + grow_gen_apply_data(ns + cnt) + i2 : ., mut = 0 + loop: + if i2 >= cnt: + break + cell_store(g_gen_apply_data, (ns + i2) * 8, cell_load(subs, i2 * 8)) + i2 = i2 + 1 + g_gen_apply_data_count = ns + cnt + return alloc_type(TYP_TUPLE, cnt, ns) + if k == TYP_GENERIC_APPLY: + st2 := get_type_extra(ti) + cnt2 := cell_load(g_gen_apply_data, st2 * 8) + subs2 : string, mut + if cnt2 > 0: + subs2 = alloc(cnt2 * 8) + i3 : ., mut = 0 + loop: + if i3 >= cnt2: + break + cell_store(subs2, i3 * 8, structpat_subst_ti(si, cell_load(g_gen_apply_data, (st2 + 1 + i3) * 8), scrut_ti)) + i3 = i3 + 1 + ns2 := g_gen_apply_data_count + grow_gen_apply_data(ns2 + 1 + cnt2) + cell_store(g_gen_apply_data, ns2 * 8, cnt2) + i4 : ., mut = 0 + loop: + if i4 >= cnt2: + break + cell_store(g_gen_apply_data, (ns2 + 1 + i4) * 8, cell_load(subs2, i4 * 8)) + i4 = i4 + 1 + g_gen_apply_data_count = ns2 + 1 + cnt2 + return alloc_type(TYP_GENERIC_APPLY, get_type_data(ti), ns2) + return ti + +// Resolve a struct field type for a pattern scrutinee. Concrete fields are +// returned unchanged; generic fields are substituted structurally (including +// optional, array, tuple, and nested generic applications). The caller rejects +// any remaining generic row for non-`_` subpatterns. +fn structpat_field_ti(si: int, jdi: int, scrut_ti: int) -> int: + if si < 0 || jdi < 0: + return -1 + ft := res_type_node(si_field_type_node(si, jdi)) + if ft < 0: + return -1 + if ti_has_generic_param(ft) == 0: + return ft + return structpat_subst_ti(si, ft, scrut_ti) + +// ─── Struct match pattern(Task 5b1)─── +// EXPR_STRUCTPAT layout: a = struct name ni, b = first wrapper, c = field count. +// wrapper.a = subpattern, wrapper.b = field name ni. Validate the pattern against +// the struct row: unknown type/field, duplicate field, and literal field type +// mismatches are hard diagnostics. Nested struct/enum/tuple subpatterns are +// rejected with TM09 rather than silently treated as irrefutable. Returns +// 0 when the pattern is usable, 1 when a diagnostic was reported. +fn check_struct_pattern(pat: int, scrut_ti: int) -> int: + if pat < 0: + return 1 + name_ni := ast_a(pat) + si := find_struct_by_name(name_ni) + if si < 0: + check_error(EC_N_UNDEFINED, + "Unknown struct type '" + istr_get(name_ni) + "' in struct pattern", + ast_line(pat), ast_col(pat)) + return 1 + // A struct pattern needs a concrete scrutinee shape. An unresolved generic + // parameter could later instantiate to a different struct (or a non-struct), + // and monomorphization would then emit field loads against the wrong object. + // Reject it here; callers must use a concrete struct type or `_`/binding. + if ti_has_generic_param(scrut_ti) != 0: + check_error(EC_TS_FIELD_TYPE, + "Struct pattern '" + istr_get(name_ni) + "' requires a concrete struct type; use a wildcard or binding arm", + ast_line(pat), ast_col(pat)) + return 1 + // Generic field types cannot be validated/bound soundly unless the scrutinee + // supplies concrete generic arguments. The per-field loop below substitutes + // direct generic parameters from the scrutinee and rejects remaining generic + // forms (except `_`, which performs no comparison or binding). + // The scrutinee must be this struct whenever its shape is statically known. + sk := get_type_kind(scrut_ti) + if sk == TYP_NAMED || sk == TYP_GENERIC_APPLY: + sni := decl_name_of_ti(scrut_ti) + if sni >= 0 && sni != name_ni: + check_error(EC_TS_FIELD_TYPE, + "Struct pattern '" + istr_get(name_ni) + "' does not match scrutinee type '" + istr_get(sni) + "'", + ast_line(pat), ast_col(pat)) + return 1 + else if sk != TYP_GENERIC_PARAM: + check_error(EC_TS_FIELD_TYPE, + "Struct pattern '" + istr_get(name_ni) + "' requires a struct scrutinee", + ast_line(pat), ast_col(pat)) + return 1 + c := ast_c(pat) + w0 := ast_b(pat) + if c > 0 && w0 < 0: + return 1 + fc := si_field_count(si) + used : string, mut = alloc((fc + 1) * 8) + j : ., mut = 0 + loop: + if j >= fc: + break + cell_store(used, j * 8, 0) + j = j + 1 + bad : ., mut = 0 + i : ., mut = 0 + loop: + if i >= c: + return bad + w := w0 + i + sub : ., mut = -1 + fni : ., mut = -1 + if w >= 0: + sub = ast_a(w) + fni = ast_b(w) + jdi := struct_field_index_by_name(si, fni) + if jdi < 0: + check_error(EC_TS_UNKNOWN_FIELD, + "Unknown field '" + istr_get(fni) + "' in struct pattern " + istr_get(name_ni), + ast_line(w), ast_col(w)) + bad = 1 + i = i + 1 + continue + if cell_load(used, jdi * 8) != 0: + check_error(EC_TS_FIELD_DUP, + "Field '" + istr_get(fni) + "' matched more than once in struct pattern " + istr_get(name_ni), + ast_line(w), ast_col(w)) + bad = 1 + cell_store(used, jdi * 8, 1) + if sub >= 0: + k := ast_kind(sub) + if k == EXPR_IDENT || k == EXPR_WILDCARD || k == EXPR_INT || k == EXPR_BOOL || k == EXPR_CHAR || k == EXPR_STRING: + ft := structpat_field_ti(si, jdi, scrut_ti) + if ft >= 0: + // Cache the checker-resolved (already substituted) field type + // on the wrapper for IR gen and monomorph clones. +1 keeps 0 + // as the "absent" sentinel because TI_INT itself is 0. + ast_set_data(w, ft + 1) + if ti_has_generic_param(ft) != 0: + if k == EXPR_IDENT: + check_error(EC_TS_FIELD_TYPE, + "Field '" + istr_get(fni) + "' in struct pattern " + istr_get(name_ni) + + " has an unresolved generic type; use a wildcard", + ast_line(sub), ast_col(sub)) + bad = 1 + else if k != EXPR_WILDCARD: + check_error(EC_TS_FIELD_TYPE, + "Field '" + istr_get(fni) + "' in struct pattern " + istr_get(name_ni) + + " has an unresolved generic type; literal patterns are not supported", + ast_line(sub), ast_col(sub)) + bad = 1 + else if k != EXPR_IDENT && k != EXPR_WILDCARD: + if get_type_kind(ft) == TYP_OPTIONAL: + check_error(EC_TS_FIELD_TYPE, + "Field '" + istr_get(fni) + "' in struct pattern " + istr_get(name_ni) + + " has an optional type; use a wildcard or binding arm", + ast_line(sub), ast_col(sub)) + bad = 1 + else: + st := infer_expr(sub) + verdict := type_compat_strict(st, ft) + if verdict != 1: + diag_type_incompatible(verdict, EC_TS_FIELD_TYPE, + "Field '" + istr_get(fni) + "' in struct pattern " + istr_get(name_ni) + + ": expected " + type_display(ft) + ", got " + type_display(st), + ast_line(sub), ast_col(sub)) + bad = 1 + else: + check_error(EC_TM_NESTED, + "Nested match patterns are not supported yet", + ast_line(sub), ast_col(sub)) + bad = 1 + i = i + 1 + return bad + +// Bind the ident subpatterns of a validated struct pattern. Each binding gets +// the declared type of its field. `_` and literal subpatterns bind nothing. +fn bind_struct_pattern_idents(pat: int, scrut_ti: int): + if pat < 0: + return + name_ni := ast_a(pat) + si := find_struct_by_name(name_ni) + c := ast_c(pat) + w0 := ast_b(pat) + if si < 0 || (c > 0 && w0 < 0): + return + i : ., mut = 0 + loop: + if i >= c: + break + w := w0 + i + if w >= 0: + sub := ast_a(w) + if sub >= 0 && ast_kind(sub) == EXPR_IDENT: + nn := ast_int_val(sub) + if str_eq(istr_get(nn), "_") == 0: + jdi := struct_field_index_by_name(si, ast_b(w)) + ft : ., mut = TI_INT + if jdi >= 0: + ft = structpat_field_ti(si, jdi, scrut_ti) + def_sym(nn, SYM_LOCAL, ft, -1) + i = i + 1 + +// ─── Tuple match pattern(Task 5b2)─── +// EXPR_TUPLEPAT layout: a = first wrapper, b = element count. wrapper.a = +// subpattern, wrapper.b = element index (0-based). The scrutinee must be a +// concrete TYP_TUPLE with the same arity. Each element is validated against the +// actual tuple element type (g_gen_apply_data at extra+i): scalar literals are +// compared strictly, ident/wildcard bind, and nested composite patterns are +// rejected with TM09 rather than silently treated as irrefutable. Returns 0 when +// the pattern is usable, 1 when a diagnostic was reported. +fn check_tuple_pattern(pat: int, scrut_ti: int) -> int: + if pat < 0: + return 1 + c := ast_b(pat) + w0 := ast_a(pat) + if c <= 0 || w0 < 0: + check_error(EC_P_PATTERN, "Malformed tuple pattern", + ast_line(pat), ast_col(pat)) + return 1 + if get_type_kind(scrut_ti) != TYP_TUPLE: + check_error(EC_TS_FIELD_TYPE, + "Tuple pattern requires a tuple scrutinee", + ast_line(pat), ast_col(pat)) + return 1 + arity := get_type_data(scrut_ti) + if c != arity: + check_error(EC_TA_TUPLE_ARITY, + "Tuple pattern has " + int_str(c) + " elements, but the scrutinee has " + int_str(arity), + ast_line(pat), ast_col(pat)) + return 1 + // A generic element has no concrete load form at codegen time. Keep the same + // conservative rule as struct patterns: require a fully concrete tuple. + if ti_has_generic_param(scrut_ti) != 0: + check_error(EC_TS_FIELD_TYPE, + "Tuple pattern requires a concrete tuple type; use a wildcard or binding arm", + ast_line(pat), ast_col(pat)) + return 1 + start := get_type_extra(scrut_ti) + bad : ., mut = 0 + i : ., mut = 0 + loop: + if i >= c: + return bad + w := w0 + i + sub : ., mut = -1 + idx : ., mut = -1 + if w >= 0: + sub = ast_a(w) + idx = ast_b(w) + if w < 0 || idx != i: + check_error(EC_P_PATTERN, "Malformed tuple pattern element index", + ast_line(pat), ast_col(pat)) + bad = 1 + i = i + 1 + continue + ft : ., mut = -1 + if start >= 0: + ft = cell_load(g_gen_apply_data, (start + i) * 8) + if ft >= 0: + // Cache the element type on the wrapper for IR lowering and monomorph + // clones. +1 keeps 0 as the "absent" sentinel because TI_INT is 0. + ast_set_data(w, ft + 1) + if sub < 0: + bad = 1 + i = i + 1 + continue + k := ast_kind(sub) + if k == EXPR_IDENT || k == EXPR_WILDCARD: + i = i + 1 + continue + if k == EXPR_INT || k == EXPR_BOOL || k == EXPR_CHAR || k == EXPR_STRING: + if ft < 0: + check_error(EC_TS_FIELD_TYPE, + "Tuple pattern element " + int_str(i) + " has no resolvable type", + ast_line(sub), ast_col(sub)) + bad = 1 + else if ti_has_generic_param(ft) != 0: + check_error(EC_TS_FIELD_TYPE, + "Tuple pattern element " + int_str(i) + " has an unresolved generic type; literal patterns are not supported", + ast_line(sub), ast_col(sub)) + bad = 1 + else if get_type_kind(ft) == TYP_OPTIONAL: + check_error(EC_TS_FIELD_TYPE, + "Tuple pattern element " + int_str(i) + " has an optional type; use a wildcard or binding arm", + ast_line(sub), ast_col(sub)) + bad = 1 + else: + st := infer_expr(sub) + verdict := type_compat_strict(st, ft) + if verdict != 1: + diag_type_incompatible(verdict, EC_TS_FIELD_TYPE, + "Tuple pattern element " + int_str(i) + ": expected " + type_display(ft) + ", got " + type_display(st), + ast_line(sub), ast_col(sub)) + bad = 1 + else: + check_error(EC_TM_NESTED, + "Nested match patterns are not supported yet", + ast_line(sub), ast_col(sub)) + bad = 1 + i = i + 1 + return bad + +// Bind the ident subpatterns of a validated tuple pattern. Each binding gets the +// actual tuple element type. `_` and literal subpatterns bind nothing. +fn bind_tuple_pattern_idents(pat: int, scrut_ti: int): + if pat < 0 || get_type_kind(scrut_ti) != TYP_TUPLE: + return + c := ast_b(pat) + w0 := ast_a(pat) + arity := get_type_data(scrut_ti) + start := get_type_extra(scrut_ti) + if w0 < 0: + return + i : ., mut = 0 + loop: + if i >= c: + break + w := w0 + i + if w >= 0: + sub := ast_a(w) + if sub >= 0 && ast_kind(sub) == EXPR_IDENT: + nn := ast_int_val(sub) + if str_eq(istr_get(nn), "_") == 0: + et : ., mut = TI_INT + if start >= 0 && i < arity: + et = cell_load(g_gen_apply_data, (start + i) * 8) + def_sym(nn, SYM_LOCAL, et, -1) + i = i + 1 + // 类型里是否仍含未实例化的泛型参数(TYP_GENERIC_PARAM,可嵌套在数组 / 指针 / 引用 / // 切片 / 元组 / 泛型应用内)。字面量的字段类型 vs 声明元素同质性判定在任一侧 // 含未实例化参数时跳过,参数未实例化时比较不成立,强行比较会假拒(泛型函数体 @@ -1653,6 +2028,8 @@ fn ti_has_generic_param(ti: int) -> int: return 1 if k == TYP_ARRAY || k == TYP_PTR || k == TYP_REF || k == TYP_SLICE: return ti_has_generic_param(get_type_data(ti)) + if k == TYP_OPTIONAL: + return ti_has_generic_param(get_type_data(ti)) if k == TYP_TUPLE: cnt := get_type_data(ti) st := get_type_extra(ti) @@ -3392,6 +3769,8 @@ fn infer_expr(node: int) -> int: mc_save := mc_begin() // 覆盖位图入栈,容量无界 m_unmappable : ., mut = 0 m_wild : ., mut = 0 // 通配/绑定已见(冗余臂判据用;非穷尽性判据本体) + m_literal : ., mut = 0 // 任一 int/bool/string/char 字面量臂已见(无限域 -> 需 irrefutable 臂) + m_order_rep : ., mut = 0 // 通配/绑定后仍有臂(TM05 只报一次) m_dup : ., mut = -1 // 首个冗余臂模式节点(-1 = 无) loop: if an < 0: @@ -3400,6 +3779,12 @@ fn infer_expr(node: int) -> int: arm_body := ast_b(an) // EXPR_ARM: b = body if arm_pat >= 0: pk := sh_match_pat_kind(arm_pat) + // 通配/绑定模式吞掉后续所有臂:后续臂不可达,报 TM05。 + if m_wild != 0 && m_order_rep == 0: + check_error(EC_TM_WILDCARD_ORDER, + "Wildcard or binding pattern must be the last arm", + ast_line(arm_pat), ast_col(arm_pat)) + m_order_rep = 1 if pk == 1: if m_wild != 0: if m_dup < 0: @@ -3407,6 +3792,16 @@ fn infer_expr(node: int) -> int: m_wild = 1 m_pats = tt_cons(tt_top(), m_pats) else if pk == 2: + // 嵌套子模式只在 Task 5b 完整支持;ident/`_` 之外的模式 + // (字面量、嵌套枚举、struct…)现在不得静默当成“变体匹配”。 + // 子模式节点不一定连续(parser 先分配复合子模式的后代), + // 因此扫描整个 payload 子树区间,而不是 ast_b + i。 + bad_nested := sh_match_payload_bad(arm_pat) + if bad_nested >= 0: + check_error(EC_TM_NESTED, + "Nested match patterns are not supported yet", + ast_line(bad_nested), ast_col(bad_nested)) + m_unmappable = 1 vi := sh_match_pat_variant(match_ti, arm_pat) vt : ., mut = -1 if vi >= 0: @@ -3433,8 +3828,36 @@ fn infer_expr(node: int) -> int: m_dup = arm_pat mc_add(ci) m_pats = tt_cons(vt, m_pats) + else if ast_kind(arm_pat) == EXPR_STRUCTPAT: + // Struct patterns are refutable: like literals, they need an + // irrefutable arm. Field syntax/type errors are diagnosed by + // check_struct_pattern; TM09 covers nested subpatterns. + m_literal = 1 + if check_struct_pattern(arm_pat, match_ti) != 0: + m_unmappable = 1 + else if ast_kind(arm_pat) == EXPR_TUPLEPAT: + // Tuple patterns are refutable as well: arity/scrutinee/type + // errors are diagnosed by check_tuple_pattern, nested composites + // as TM09. An irrefutable arm is still required. + m_literal = 1 + if check_tuple_pattern(arm_pat, match_ti) != 0: + m_unmappable = 1 else: - m_unmappable = 1 // 字面量/struct 模式,因此不判(登记) + lit_k := sh_match_lit_kind(arm_pat) + if lit_k >= 0: + m_literal = 1 + if sh_match_lit_seen(first_arm, an, lit_k, ast_int_val(arm_pat)) != 0: + if m_dup < 0: + m_dup = arm_pat + else: + // 插值字符串 pattern 不是字面量:parser 产出 EXPR_BINARY/ + // EXPR_INTERP_HOLE。它既不能按值比较,也不得当成“任意串匹配”。 + ak0 := ast_kind(arm_pat) + if ak0 == EXPR_BINARY || ak0 == EXPR_INTERP_HOLE: + check_error(EC_TM_NESTED, + "String interpolation is not supported in a match pattern; use a literal", + ast_line(arm_pat), ast_col(arm_pat)) + m_unmappable = 1 // struct / 插值 / EXPR_BIGINT 等,不判穷尽 // Bind pattern variables in new scope push_scope() if arm_pat >= 0: @@ -3452,8 +3875,18 @@ fn infer_expr(node: int) -> int: def_sym(ast_int_val(spn), SYM_LOCAL, TI_INT, -1) spn = spn + 1 spi = spi + 1 + if ast_kind(arm_pat) == EXPR_STRUCTPAT: + // Struct pattern field bindings: each ident subpattern gets the + // declared field type (Task 5b1). + bind_struct_pattern_idents(arm_pat, match_ti) + if ast_kind(arm_pat) == EXPR_TUPLEPAT: + // Tuple pattern element bindings: each ident subpattern gets the + // actual tuple element type (Task 5b2). + bind_tuple_pattern_idents(arm_pat, match_ti) if ast_kind(arm_pat) == EXPR_IDENT: - def_sym(ast_int_val(arm_pat), SYM_LOCAL, TI_INT, -1) + // 绑定臂的值就是 scrutinee:类型取 scrutinee 类型(旧实现 + // 一律 TI_INT,字符串/char/bool scrutinee 会错型)。 + def_sym(ast_int_val(arm_pat), SYM_LOCAL, match_ti, -1) push_borrow_scope() arm_ti := infer_expr(arm_body) pop_borrow_scope() @@ -3466,12 +3899,24 @@ fn infer_expr(node: int) -> int: // 判定结果:0 = 不穷尽(诊断 + 具体反例变体名);1 = 穷尽;-1 = 不判(非枚举域/不可映射 // 模式/引擎未知),零诊断(判定约定:未知不得当不穷尽,也不得当穷尽)。 ty_budget_reset(200000) - m_verdict := sh_match_exhaustive(match_ti, m_pats, m_unmappable) + m_verdict : ., mut = -1 + if m_unmappable == 0 && m_literal != 0: + // 字面量域(int/string/char)无限;bool 也按设计要求显式列出 `_`/绑定臂。 + // 没有 irrefutable 臂 -> 不穷尽,不再静默放行。 + if m_wild != 0: + m_verdict = 1 + else: + m_verdict = 0 + else: + m_verdict = sh_match_exhaustive(match_ti, m_pats, m_unmappable) ty_budget_reset(200000) if m_verdict == 0: mi : ., mut = -1 msg := "Non-exhaustive match" - if get_type_kind(match_ti) == TYP_OPTIONAL: + if m_literal != 0: + // 字面量域没有具体变体可命名;保持与枚举反例同一诊断码。 + msg = msg + ": add a wildcard or binding arm" + else if get_type_kind(match_ti) == TYP_OPTIONAL: // `T?` 两分支反例命名:0 = 有值(Some)/ 1 = null(None) mi = mc_first_missing(2) if mi == 0: @@ -3948,6 +4393,13 @@ fn infer_expr(node: int) -> int: return get_type_data(inner_ti) return inner_ti if ast_kind(node) == EXPR_STRUCTPAT: + // Patterns are not value expressions. Match-arm validation/binding lives + // in the EXPR_MATCH branch (check_struct_pattern / bind_struct_pattern_idents). + return TI_UNIT + if ast_kind(node) == EXPR_TUPLEPAT: + // Tuple patterns are not value expressions either. Match-arm + // validation/binding lives in EXPR_MATCH (check_tuple_pattern / + // bind_tuple_pattern_idents). return TI_UNIT if ast_kind(node) == EXPR_AS: // expr as Type — type cast diff --git a/src/compiler/cir_cache.cr b/src/compiler/cir_cache.cr index 7b71eaf9..2f298bd7 100644 --- a/src/compiler/cir_cache.cr +++ b/src/compiler/cir_cache.cr @@ -16,6 +16,14 @@ // {kind, enter_rel, exit_rel, parent_rel, nstart_rel, ncount} // edges: count, then 24-byte records {rel_from, rel_to, kind} // instructions: count, then 48-byte records {op, dest, s1, s2, s3, tk} +// match cases: match count, then per IR_MATCH in instruction order: +// {case_count, then case_count x 24-byte records +// value i64, kind u32, str i32, target_label i32, flags u32} +// This is the semantic model, not the derived wire: target_label is +// the IR body label number, or -1 when the target is unresolved. +// The MATCH s2/s3 stored in nodes/instructions are model start/count; +// the loader rewrites s2 to the reader's model start and ccr_lower +// derives the wire CASE table afterwards. // string constants: count, then {length, bytes} // intern journal: base count, record count, then {length, bytes} // trailer: var_start, node_start @@ -53,6 +61,9 @@ // NOD type ids) does not change this snapshot layout or its derived-code // contract. The stored records, journal, and trailer are unchanged, so no // version bump is required for that migration; only `.ccr` bytes changed. +// v24 adds the per-function MATCH CASE section after the instruction range. +// v25 changes that section from derived wire rows (target NOD id) to the +// semantic model (target body label), so v24 entries are invalid. // // Magic value: // The magic is a negative 64-bit constant written as signed decimal. The @@ -60,7 +71,7 @@ // writer and reader use the same constant, so the cache remains self-consistent. // The limitation is recorded in the project backlog. CIR_CACHE_MAGIC : int = -4485090715960753727 -CIR_CACHE_VER : int = 23 +CIR_CACHE_VER : int = 25 g_cir_write_buf : string, mut g_cir_write_pos : int, mut @@ -195,10 +206,6 @@ fn save_cir_cache(path: string, source_fi: int, ir_fi: int) -> int: if cid == 0: return -1 - fd := syscall3(2, path, 577, 420) // O_WRONLY|O_CREAT|O_TRUNC, 0644 - if fd < 0: - return -1 - // Compute fingerprint fn_node := fi_ast_node(source_fi) fp := func_fingerprint(fn_node) @@ -214,6 +221,33 @@ fn save_cir_cache(path: string, source_fi: int, ir_fi: int) -> int: instr_start := cell_load(g_ir_func_instr_start, ir_fi * 8) instr_count := cell_load(g_ir_func_instr_count, ir_fi * 8) + // Validate and size the MATCH CASE section before touching the file. Every + // IR_MATCH owns a contiguous range of semantic model rows (g_lat_case); + // nested matches append after their enclosing range, which is also + // instruction order. A mismatch would produce a snapshot the loader must + // reject, so refuse to write it. + match_count : ., mut = 0 + match_size : ., mut = 8 + expected_case : ., mut = -1 + ms_i : ., mut = 0 + loop: + if ms_i >= instr_count: + break + ms_inst := instr_start + ms_i + if iri_op(ms_inst) == IR_MATCH: + ms_start := iri_s2(ms_inst) + ms_n := iri_s3(ms_inst) + if ms_start < 0 || ms_n < 0: + return -1 + if ms_start + ms_n > g_lat_case_count: + return -1 + if expected_case >= 0 && ms_start != expected_case: + return -1 + expected_case = ms_start + ms_n + match_count = match_count + 1 + match_size = match_size + 8 + ms_n * 24 + ms_i = ms_i + 1 + // Serialize in memory and issue one write. The previous per-field writes // made a full self-host build perform millions of syscalls on its cache. total_size : ., mut = 48 + name_len // v17 头部 = magic/ver/identity/fp/sig/name_len @@ -232,6 +266,9 @@ fn save_cir_cache(path: string, source_fi: int, ir_fi: int) -> int: own_edge_count = 0 total_size = total_size + 8 + own_edge_count * 24 total_size = total_size + 8 + instr_count * 48 + // Per-function MATCH CASE model section. Always present, even when empty, + // so the reader can parse every v25 snapshot with one layout. + total_size = total_size + match_size total_size = total_size + 8 size_si : ., mut = 0 loop: @@ -263,6 +300,10 @@ fn save_cir_cache(path: string, source_fi: int, ir_fi: int) -> int: g_cir_write_cap = new_cap g_cir_write_pos = 0 + fd := syscall3(2, path, 577, 420) // O_WRONLY|O_CREAT|O_TRUNC, 0644 + if fd < 0: + return -1 + // Write header cir_store_cell(fd, CIR_CACHE_MAGIC) cir_store_cell(fd, CIR_CACHE_VER) @@ -354,6 +395,34 @@ fn save_cir_cache(path: string, source_fi: int, ir_fi: int) -> int: cir_store_cell(fd, iri_tk(inst)) ii = ii + 1 + // Write the MATCH CASE section. Matches are stored in instruction order so + // the loader can pair each group with its IR_MATCH without storing an index. + cir_store_cell(fd, match_count) + cs_i : ., mut = 0 + loop: + if cs_i >= instr_count: + break + cs_inst := instr_start + cs_i + if iri_op(cs_inst) == IR_MATCH: + cs_start := iri_s2(cs_inst) + cs_n := iri_s3(cs_inst) + cir_store_cell(fd, cs_n) + cj : ., mut = 0 + loop: + if cj >= cs_n: + break + cbase := (cs_start + cj) * 24 + cir_store_cell(fd, cell_load(g_lat_case, cbase + CCR_CASE_VALUE)) + cir_store_cell32(fd, cell_load32(g_lat_case, cbase + CCR_CASE_KIND)) + cir_store_cell32(fd, cell_load_i32(g_lat_case, cbase + CCR_CASE_STR)) + // Persist the semantic target label, not a node-relative wire + // target. Labels are restored verbatim, so no baseline rebase + // is needed on load. + cir_store_cell32(fd, cell_load_i32(g_lat_case, cbase + CCR_CASE_TARGET)) + cir_store_cell32(fd, cell_load32(g_lat_case, cbase + CCR_CASE_FLAGS)) + cj = cj + 1 + cs_i = cs_i + 1 + // Write string constants used by this function cir_store_cell(fd, g_ir_str_const_count) si : ., mut = 0 @@ -411,6 +480,12 @@ fn cir_store_cell(fd: int, val: int): cell_store(g_cir_write_buf, g_cir_write_pos, val) g_cir_write_pos = g_cir_write_pos + 8 +// CASE records pack 32-bit fields after the 64-bit value. Keep them at their +// wire widths; widening them to 8 bytes would break the shared 24B row. +fn cir_store_cell32(fd: int, val: int): + cell_store32(g_cir_write_buf, g_cir_write_pos, val) + g_cir_write_pos = g_cir_write_pos + 4 + fn cir_store_byte(fd: int, val: int): cell_store_byte(g_cir_write_buf, g_cir_write_pos, val) g_cir_write_pos = g_cir_write_pos + 1 @@ -567,7 +642,116 @@ fn load_cir_cache(path: string, func_idx: int) -> int: p = p + 8 + p_instr_count * 48 if p > dlen: return -1 + // emit() creates one NOD and one instruction per operation, so the local + // index is the same in both sections. The MATCH NOD mirror is validated below + // through this mapping; require the counts to agree before relying on it. + if p_node_count != p_instr_count: + return -1 + // Count IR_MATCH instructions. The CASE section stores one group per MATCH + // in instruction order, so this count must match the section header exactly. + p_match_count : ., mut = 0 + p_mi : ., mut = 0 + loop: + if p_mi >= p_instr_count: + break + if cell_load(data, instr_p + 8 + p_mi * 48) == IR_MATCH: + p_match_count = p_match_count + 1 + p_mi = p_mi + 1 + + // MATCH CASE section: match count, then per MATCH {case_count, rows}. This + // is the semantic model: a row stores target_label = the IR body label + // number. Validate every target against this function's IR_LABEL nodes + // before any global is written. + case_p := p + if p + 8 > dlen: + return -1 + p_case_matches := cell_load(data, p) + p = p + 8 + if p_case_matches != p_match_count: + return -1 + p_ci : ., mut = 0 + p_mi = 0 + p_expected_case : ., mut = -1 + loop: + if p_ci >= p_case_matches: + break + // Pair this group with the next IR_MATCH in instruction order. + loop: + if p_mi >= p_instr_count: + return -1 + if cell_load(data, instr_p + 8 + p_mi * 48) == IR_MATCH: + break + p_mi = p_mi + 1 + p_inst := instr_p + 8 + p_mi * 48 + p_ms := cell_load(data, p_inst + 24) + p_mn := cell_load(data, p_inst + 32) + if p + 8 > dlen: + return -1 + p_cn := cell_load(data, p) + p = p + 8 + if p_cn != p_mn || p_cn < 0 || p_ms < 0: + return -1 + // The cached MATCH NOD is lowered into the .ccr, so its s2/s3 must + // mirror the instruction exactly. A tampered NOD would otherwise hit and + // change the warm CASE range after lower_to_ccr rebuilds from NOD. + p_nod := node_p + 8 + p_mi * 64 + if cell_load(data, p_nod) != IR_MATCH: + return -1 + if cell_load(data, p_nod + 24) != p_ms: + return -1 + if cell_load(data, p_nod + 32) != p_mn: + return -1 + // Matches in one function own contiguous ranges in increasing order. + if p_expected_case >= 0 && p_ms != p_expected_case: + return -1 + p_expected_case = p_ms + p_cn + if p_cn > (dlen - p) / 24: + return -1 + p_rj : ., mut = 0 + loop: + if p_rj >= p_cn: + break + p_kind := cell_load32(data, p + 8) + p_flags := cell_load32(data, p + 20) + if p_kind < CCR_CASE_KIND_VALUE || p_kind > CCR_CASE_KIND_DEFAULT: + return -1 + if p_flags != 0: + return -1 + p_tl := cell_load_i32(data, p + 16) + if p_tl < -1: + return -1 + // A non-negative model target must name an IR_LABEL node in this + // function. -1 is preserved for an unresolved target and will be + // rejected by ccr_lower's wire validation instead of restoring a + // bogus label. + if p_tl >= 0: + p_lf : ., mut = 0 + p_ln : ., mut = 0 + loop: + if p_ln >= p_node_count: + break + p_loff := node_p + 8 + p_ln * 64 + if cell_load(data, p_loff) == IR_LABEL && cell_load(data, p_loff + 16) == p_tl: + p_lf = 1 + break + p_ln = p_ln + 1 + if p_lf == 0: + return -1 + p = p + 24 + p_rj = p_rj + 1 + p_mi = p_mi + 1 + p_ci = p_ci + 1 + // No unmatched IR_MATCH may remain after the stored groups. + loop: + if p_mi >= p_instr_count: + break + if cell_load(data, instr_p + 8 + p_mi * 48) == IR_MATCH: + return -1 + p_mi = p_mi + 1 + str_p := p + if p + 8 > dlen: + return -1 p_str_count := cell_load(data, p) p = p + 8 p_si : ., mut = 0 @@ -692,6 +876,31 @@ fn load_cir_cache(path: string, func_idx: int) -> int: str_intern(js) ji = ji + 1 + // CASE str indexes are intern ids, so they are meaningful only after the + // journal replay above. Range-check them before the destructive array restore; + // do not call istr_get here because a malformed id must stay a cache miss. + if p_case_matches > 0: + csp : ., mut = case_p + 8 + csi : ., mut = 0 + loop: + if csi >= p_case_matches: + break + csn := cell_load(data, csp) + csp = csp + 8 + csr : ., mut = 0 + loop: + if csr >= csn: + break + csidx := cell_load_i32(data, csp + 12) + cskind := cell_load32(data, csp + 8) + if csidx < -1 || csidx >= g_str_count: + return -1 + if cskind == CCR_CASE_KIND_STRING && csidx < 0: + return -1 + csp = csp + 24 + csr = csr + 1 + csi = csi + 1 + // Restore vars var_count := cell_load(data, pos) pos = pos + 8 @@ -865,6 +1074,50 @@ fn load_cir_cache(path: string, func_idx: int) -> int: ii = ii + 1 g_ir_instr_count = base_instr + instr_count + // Restore this function's semantic MATCH CASE ranges. The section is in the + // same order as the restored instruction stream, so pair each group with the + // next IR_MATCH and append at the current global model count. Targets are + // already body label numbers and labels are restored verbatim, so only the + // model start is rebased. s3 (the count) is unchanged. This restores the + // model, not the wire; ccr_lower derives g_ccr_case afterwards. The pre-scan + // already validated counts, ordering, target labels, and row domains, so + // this phase cannot fail halfway. + restored_matches := cell_load(data, pos) + pos = pos + 8 + rm_i : ., mut = 0 + rm_scan : ., mut = base_instr + loop: + if rm_i >= restored_matches: + break + loop: + if rm_scan >= g_ir_instr_count: + return -1 + if iri_op(rm_scan) == IR_MATCH: + break + rm_scan = rm_scan + 1 + rm_count := cell_load(data, pos) + pos = pos + 8 + rm_case_start := g_lat_case_count + iri_set_s2(rm_scan, rm_case_start) + // lower_to_ccr rebuilds instructions from NOD, so the mirror must get the + // same model start. Node/instr local indexes are 1:1 (checked above). + rm_nod := base_node + (rm_scan - base_instr) + cell_store(g_df_nodes, rm_nod * ESZ_DFNODE + OFF_DF_S2, rm_case_start) + rm_j : ., mut = 0 + loop: + if rm_j >= rm_count: + break + rm_value := cell_load(data, pos) + rm_kind := cell_load32(data, pos + 8) + rm_str := cell_load_i32(data, pos + 12) + rm_label := cell_load_i32(data, pos + 16) + rm_flags := cell_load32(data, pos + 20) + pos = pos + 24 + lat_case_append(rm_value, rm_kind, rm_str, rm_label, rm_flags) + rm_j = rm_j + 1 + rm_scan = rm_scan + 1 + rm_i = rm_i + 1 + // Restore string constants (used by nodes/instrs) str_count := cell_load(data, pos) pos = pos + 8 diff --git a/src/compiler/dataflow.cr b/src/compiler/dataflow.cr index 86e11b25..813cf289 100644 --- a/src/compiler/dataflow.cr +++ b/src/compiler/dataflow.cr @@ -339,6 +339,11 @@ fn df_connect_srcs(node_id: int, opcode: int, s1: int, s2: int, s3: int, tk_aux: if opcode == IR_BRANCH: df_use_var(node_id, s1) // condition var (labels s2, s3 are not vars) return + if opcode == IR_MATCH: + // The scrutinee is the only variable read; case targets are labels in + // the CASE table, not node operands. + df_use_var(node_id, s1) + return if opcode == IR_BOUNDS_CHECK: df_use_var(node_id, s1) // 单槽化:旗标已从混用 tk 槽移入辅码槽 -> 读 tk_aux(值域不变: @@ -597,6 +602,8 @@ fn df_opcode_name(opcode: int, s3: int) -> string: return "ref" if opcode == IR_BRANCH: return "branch" + if opcode == IR_MATCH: + return "match" if opcode == IR_JUMP: return "jump" if opcode == IR_LABEL: diff --git a/src/compiler/dump.cr b/src/compiler/dump.cr index 4c36ae9a..990b83f6 100644 --- a/src/compiler/dump.cr +++ b/src/compiler/dump.cr @@ -193,6 +193,11 @@ fn ir_instr_str(instr_idx: int) -> string: if iri_op(instr_idx) == IR_BRANCH: s = s + "if " + ir_var_str(iri_s1(instr_idx)) + " goto label" + int_str(iri_s2(instr_idx)) + " else label" + int_str(iri_s3(instr_idx)) return s + if iri_op(instr_idx) == IR_MATCH: + // CASE rows live in the global table; print the range so the cir dump + // and --dump-objects agree on which rows belong to this node. + s = s + "match " + ir_var_str(iri_s1(instr_idx)) + " cases " + int_str(iri_s2(instr_idx)) + ".." + int_str(iri_s2(instr_idx) + iri_s3(instr_idx)) + return s if iri_op(instr_idx) == IR_JUMP: s = s + "goto label" + int_str(iri_s1(instr_idx)) return s diff --git a/src/compiler/globals.cr b/src/compiler/globals.cr index a67f9f77..0dae347a 100644 --- a/src/compiler/globals.cr +++ b/src/compiler/globals.cr @@ -60,6 +60,31 @@ g_ccr_nod_type : string, mut g_ccr_nod_type_count : int, mut g_ccr_nod_flags : string, mut g_ccr_nod_flags_count : int, mut +// Semantic v10 CASE model. Rows are 24B records with the same field layout as +// the wire (value, kind, str index, target, flags), but `target` is the IR body +// *label number* rather than a NOD id. ir_gen fills this model; ccr_lower is +// the only step that turns it into g_ccr_case (the wire table), and the CIR +// cache persists this model rather than the derived wire. This file is first in +// both builds, so these globals must stay here. +g_lat_case : string, mut +g_lat_case_count : int, mut +// Capacity of g_lat_case in 24B rows. Compiler-private and never serialized. +g_lat_case_cap : int, mut +// In-memory CASE table for v10 segment tag 9. Rows are 24B records with the +// layout named by CCR_CASE_* (value, kind, str index, target NOD, flags). +// This is a derived buffer: ccr_lower builds it from g_lat_case, and ccr_io / +// interp read it as wire. The empty program keeps count zero and the wire +// writes a bare 4B zero count. The loader rebuilds the same rows from disk. +// This file is first in both builds, so it must stay here. +g_ccr_case : string, mut +g_ccr_case_count : int, mut +// Capacity of g_ccr_case in 24B rows. ccr_lower appends rows while deriving the +// wire; the capacity is compiler-private and never serialized. +g_ccr_case_cap : int, mut +// Set by ir_gen when the current function lowered a match to IR_MATCH. The +// .cir snapshot carries the semantic CASE range, so this is a diagnostic flag, +// not a cache-save gate. +g_cir_has_match : int, mut g_ast : string, mut g_ast_count : int, mut g_ast_cap : int, mut diff --git a/src/compiler/interp.cr b/src/compiler/interp.cr index acaaaf8b..df35f0a9 100644 --- a/src/compiler/interp.cr +++ b/src/compiler/interp.cr @@ -159,6 +159,26 @@ fn ir_interp_binary(d: int, s1: int, s2: int, s3: int, ti: int): cell_store(g_ir_vals, d * 8, lv % rv) return + if ti == TI_BOOL || irv_type(s1) == TI_BOOL || irv_type(s2) == TI_BOOL: + // Bool slots are raw 0/1, not big-int tagged. ELF's emit_big_binary is + // gated on both operands being TI_INT, so the interpreter must also + // compare these raw instead of calling bi_cmp. `ti` is the result code: + // a bool-returning comparison can still take int operands (for example + // the optional-equality `eqi != _cz`). + if s3 == 6: + cell_store(g_ir_vals, d * 8, lv == rv) + else if s3 == 7: + cell_store(g_ir_vals, d * 8, lv != rv) + else if s3 == 8: + cell_store(g_ir_vals, d * 8, lv < rv) + else if s3 == 9: + cell_store(g_ir_vals, d * 8, lv > rv) + else if s3 == 10: + cell_store(g_ir_vals, d * 8, lv <= rv) + else if s3 == 11: + cell_store(g_ir_vals, d * 8, lv >= rv) + return + if s3 == 1: cell_store(g_ir_vals, d * 8, bi_add(lv, rv)) else if s3 == 2: @@ -313,6 +333,46 @@ fn ir_interp_call(d: int, s1: int, s2: int, s3: int): break cfi = cfi + 1 +// v10 MATCH dispatch. Returns the NOD id of the selected CASE target, or -1 +// when the table has no default and no value matched. The runtime forms follow +// the existing IR ops: enum scrutinees are pointers whose tag word is the +// variant-name index; int scrutinees are tagged when big-int mode is on; bool +// scrutinees stay raw. +fn interp_match_target(scrutinee: int, scrutinee_ti: int, case_start: int, case_count: int) -> int: + if scrutinee < 0 || case_start < 0 || case_count < 0: + return -1 + cv := cell_load(g_ir_vals, scrutinee * 8) + default_t : ., mut = -1 + ci : ., mut = 0 + loop: + if ci >= case_count: + break + base := (case_start + ci) * CCR_V10_CASE_DISK + kind := cell_load32(g_ccr_case, base + CCR_CASE_KIND) + val := cell_load(g_ccr_case, base + CCR_CASE_VALUE) + target := cell_load_i32(g_ccr_case, base + CCR_CASE_TARGET) + if kind == CCR_CASE_KIND_DEFAULT: + default_t = target + else if kind == CCR_CASE_KIND_ENUM: + tag : ., mut = 0 + if cv != 0: + unsafe: + tag = cell_load(@str_of(cv), 0) + if g_interp_big != 0: + if ir_interp_tag(tag) == ir_interp_tag(val): + return target + else: + if tag == val: + return target + else: + rv := val + if g_interp_big != 0 && scrutinee_ti == TI_INT: + rv = ir_interp_tag(val) + if cv == rv: + return target + ci = ci + 1 + return default_t + // Inline execution of a callee graph. Both the main loop and nested callees use // this function. It builds the label table, copies arguments, executes f_count // nodes, restores the label table, and returns the IR_RETURN value. A function @@ -669,6 +729,20 @@ fn ir_interp_run_fn(cfi: int, arg_base: int, argc: int) -> int: if op2 == 20: if t1 >= 0 && t1 < g_label_count: ip2 = cell_load(g_label_poses, t1 * 8) + // v10 IR_MATCH: dispatch on the scrutinee and jump to the CASE target. + // The callee loop increments ip2 at the bottom, so point one before the + // target NOD (the target is itself a LABEL node). A miss with no + // DEFAULT and an out-of-span target both trap. + if op2 == 54: + mt2 := interp_match_target(t1, irv_type(t1), t2, t3) + if mt2 < 0: + g_interp_abort = -1 + ip2 = f_count + else if mt2 >= f_start && mt2 < f_start + f_count: + ip2 = mt2 - f_start - 1 + else: + g_interp_abort = -1 + ip2 = f_count ip2 = ip2 + 1 // Restore label state g_label_count = old_lc @@ -1127,6 +1201,19 @@ fn ir_interpret() -> int: continue else: break + // v10 IR_MATCH: the CASE table selects a LABEL node and control jumps + // directly there. ip is function-relative; target is a global NOD id. + // A miss with no DEFAULT and an out-of-span target both trap. + if op == 54: + mt := interp_match_target(s1, irv_type(s1), s2, s3) + if mt < 0: + g_interp_abort = -1 + return -1 + if mt < node_start || mt >= node_start + node_count: + g_interp_abort = -1 + return -1 + ip = mt - node_start + continue // IR_LABEL: no operation. // IR_AWAIT diff --git a/src/compiler/ir_gen.cr b/src/compiler/ir_gen.cr index 21246a5f..e8de3190 100644 --- a/src/compiler/ir_gen.cr +++ b/src/compiler/ir_gen.cr @@ -827,6 +827,43 @@ fn new_label() -> int: g_next_label = g_next_label + 1 return lbl +// ── v10 semantic CASE model helpers ────────────────────────────────────── +// The model is a flat 24B-row buffer with the same layout as the wire CASE +// table, except `target` is the IR body label number (not a NOD id). A match +// reserves its rows before emitting arm bodies so nested matches append after +// the outer range; each target label is recorded once its body label exists. +// ccr_lower resolves labels to NOD ids and builds the wire table; ir_gen never +// writes g_ccr_case. +fn grow_lat_case(needed: int): + if needed < g_lat_case_cap: + return + nc : ., mut = g_lat_case_cap * 2 + if nc < 16: + nc = 16 + if nc < needed: + nc = needed + 16 + nb := alloc(nc * CCR_V10_CASE_DISK) + cell_copy(g_lat_case, g_lat_case_cap * CCR_V10_CASE_DISK, nb) + g_lat_case = nb + g_lat_case_cap = nc + +fn lat_case_append(value: int, kind: int, str_index: int, target_label: int, flags: int) -> int: + idx := g_lat_case_count + grow_lat_case(idx + 1) + base := idx * CCR_V10_CASE_DISK + cell_store(g_lat_case, base + CCR_CASE_VALUE, value) + cell_store32(g_lat_case, base + CCR_CASE_KIND, kind) + cell_store32(g_lat_case, base + CCR_CASE_STR, str_index) + cell_store32(g_lat_case, base + CCR_CASE_TARGET, target_label) + cell_store32(g_lat_case, base + CCR_CASE_FLAGS, flags) + g_lat_case_count = idx + 1 + return idx + +fn lat_case_set_target(idx: int, label: int): + if idx < 0 || idx >= g_lat_case_count: + return + cell_store32(g_lat_case, idx * CCR_V10_CASE_DISK + CCR_CASE_TARGET, label) + fn bind_local(name_idx: int, var_idx: int): grow_ir_locals(g_ir_local_count + 1) cell_store(g_ir_locals, g_ir_local_count * 16, name_idx) @@ -853,6 +890,147 @@ fn find_global(name_idx: int) -> int: i = i - 1 return -1 +// Field load form for struct-pattern tests/bindings. Aggregate reads lose the +// declared type; string patterns must keep TI_STR so str_eq compares contents, +// bool/char keep their declared form so literal comparisons use the same +// representation as the existing bool/char arms, and other fields use the exact +// integer slot form (TI_INT). +// Field load form for struct-pattern tests/bindings. `ft` is the checker-resolved +// field type stored on the wrapper (+1). String patterns must keep TI_STR so +// str_eq compares contents, bool/char keep their declared form so literal +// comparisons use the same representation as the existing arms, and other fields +// use the exact integer slot form (TI_INT). Optional fields are raw slots. +fn structpat_field_load_ti_from(ft: int) -> int: + if ft == TI_STR: + return TI_STR + if ft == TI_DEX: + return TI_DEX_S + if ft == TI_BOOL: + return TI_BOOL + if ft == TI_CHAR: + return TI_CHAR + return TI_INT + +// ─── Struct match pattern lowering(Task 5b1)─── +// Emit the comparison for one field subpattern. `fall` is taken when the +// comparison fails. Returns 1 when a test was emitted, 0 for ident/wildcard +// (no test), and -1 for an unsupported nested subpattern (caller must fail +// closed instead of jumping to the body). +fn gen_structpat_field_test(sub: int, fv: int, fall: int) -> int: + if sub < 0: + return 0 + k := ast_kind(sub) + if k == EXPR_IDENT || k == EXPR_WILDCARD: + return 0 + ok_lbl := new_label() + cmp_var : ., mut = -1 + if k == EXPR_INT: + pat_val := new_ir_var("pval", TI_INT) + emit(IR_CONST, pat_val, ast_int_val(sub), 0, 0, TI_INT) + cmp_var = new_ir_var("cmp", TI_INT) + emit(IR_BINARY, cmp_var, fv, pat_val, OP_EQ, 0) + else if k == EXPR_BOOL: + pat_val := new_ir_var("pval", TI_BOOL) + pat_bool : ., mut = 0 + if ast_int_val(sub) != 0: + pat_bool = 1 + emit(IR_CONST, pat_val, pat_bool, 0, 0, TI_BOOL) + cmp_var = new_ir_var("cmp", TI_INT) + emit(IR_BINARY, cmp_var, fv, pat_val, OP_EQ, 0) + else if k == EXPR_CHAR: + // char runtime value = interned string index; the pattern stores the same + // index in ast_int_val (same contract as the existing char arm). + pat_val := new_ir_var("pval", TI_CHAR) + track_str(ast_int_val(sub)) + emit(IR_CONST, pat_val, ast_int_val(sub), 0, 0, TI_CHAR) + cmp_var = new_ir_var("cmp", TI_INT) + emit(IR_BINARY, cmp_var, fv, pat_val, OP_EQ, 0) + else if k == EXPR_STRING: + pat_val := new_ir_var("spat", TI_STR) + track_str(ast_int_val(sub)) + emit(IR_CONST, pat_val, ast_int_val(sub), 0, 0, TI_STR) + eq0 := new_ir_var("_seq0", TI_STR) + eq1 := new_ir_var("_seq1", TI_STR) + emit(IR_STORE, -1, eq0, fv, 0, 0) + emit(IR_STORE, -1, eq1, pat_val, 0, 0) + cmp_var = new_ir_var("cmp", TI_BOOL) + emit(IR_CALL, cmp_var, eq0, 2, str_intern("str_eq"), TI_BOOL) + else: + return -1 + emit(IR_BRANCH, -1, cmp_var, ok_lbl, fall, 0) + emit(IR_LABEL, -1, ok_lbl, 0, 0, 0) + return 1 + +// Bind ident subpatterns for a struct pattern in the arm scope. Each binding +// reads its field again and gets the checker-resolved field type stored on the +// wrapper. Literal and `_` subpatterns bind nothing. +fn gen_structpat_binds(pat: int, match_val: int): + if pat < 0 || match_val < 0: + return + si := find_struct_by_name(ast_a(pat)) + c := ast_c(pat) + w0 := ast_b(pat) + if si < 0 || w0 < 0: + return + i : ., mut = 0 + loop: + if i >= c: + break + w := w0 + i + if w >= 0: + sub := ast_a(w) + if sub >= 0 && ast_kind(sub) == EXPR_IDENT: + nn := ast_int_val(sub) + if str_eq(istr_get(nn), "_") == 0: + jdi := struct_field_index_by_name(si, ast_b(w)) + if jdi >= 0: + fd := ast_data(w) + ft : ., mut = struct_row_field_ti(si, jdi) + if fd > 0: + ft = fd - 1 + // A still-generic field cannot be bound soundly; the + // pattern was fail-closed before the body. + if ti_has_generic_param(ft) == 0: + fv := new_ir_var("fld", TI_INT) + irv_set_type(fv, structpat_field_load_ti_from(ft)) + if get_type_kind(ft) == TYP_OPTIONAL && g_optrep_on != 0: + irv_set_decl_ti(fv, ft) + emit(IR_LOAD_FIELD, fv, match_val, 0, jdi, 0) + bind_local(nn, fv) + i = i + 1 + +// Bind ident subpatterns for a tuple pattern in the arm scope. Each binding reads +// its element again and gets the checker-resolved element type stored on the +// wrapper. Literal and `_` subpatterns bind nothing. +fn gen_tuplepat_binds(pat: int, match_val: int): + if pat < 0 || match_val < 0: + return + c := ast_b(pat) + w0 := ast_a(pat) + if w0 < 0: + return + i : ., mut = 0 + loop: + if i >= c: + break + w := w0 + i + if w >= 0: + sub := ast_a(w) + if sub >= 0 && ast_kind(sub) == EXPR_IDENT: + nn := ast_int_val(sub) + if str_eq(istr_get(nn), "_") == 0: + fd := ast_data(w) + ft : ., mut = TI_INT + if fd > 0: + ft = fd - 1 + fv := new_ir_var("fld", TI_INT) + irv_set_type(fv, structpat_field_load_ti_from(ft)) + if get_type_kind(ft) == TYP_OPTIONAL && g_optrep_on != 0: + irv_set_decl_ti(fv, ft) + emit(IR_LOAD_FIELD, fv, match_val, 0, ast_b(w), 0) + bind_local(nn, fv) + i = i + 1 + // Return the literal initializer AST node for an immutable module-level // constant. The ELF backend has no data initializers, so leaving these as // BSS globals turns values such as T_FN and ESZ_TOKEN into zero at runtime. @@ -2694,7 +2872,157 @@ fn gen_expr(node: int) -> int: emit(IR_CONST, mrep_var, 1, 0, 0, TI_INT) irv_set_rep(result_var, mrep_var) merge_lbl := new_label() + // ── Supported-pattern pre-scan ─────────────────────────────────── + // One IR_MATCH requires every arm to be encodable in the CASE table: + // int/bool literals, enum variants without sub-patterns, and one last + // wildcard. Anything else keeps the sequential lowering unchanged. + arm_total : ., mut = 0 + an0 := first_arm + loop: + if an0 < 0: + break + arm_total = arm_total + 1 + an0 = ast_c(an0) + match_mode : ., mut = 1 + if first_arm < 0: + match_mode = 0 + // 可选表示位分派(Some/None 的裸/装箱两态)保留既有逐臂路径。 + if g_optrep_on != 0 && match_val >= 0: + if irv_rep(match_val) >= 0: + match_mode = 0 + // Pass A: classify support without resolving enum variant names. This + // keeps an unsupported match's string-interning order byte-identical to + // the old sequential lowering. + wild_total : ., mut = 0 + saw_int : ., mut = 0 + saw_bool : ., mut = 0 + saw_enum : ., mut = 0 + an0 = first_arm + loop: + if an0 < 0: + break + ap0 := ast_a(an0) + k0 : ., mut = -1 + if ap0 >= 0: + k0 = ast_kind(ap0) + if k0 == EXPR_INT: + saw_int = 1 + iv0 := ast_int_val(ap0) + // The backend tags small integers as value<<1. Values outside + // the 62-bit immediate window need the big-int path, which this + // task does not encode in CASE. + if iv0 > 4611686018427387903 || iv0 < -4611686018427387903: + match_mode = 0 + else if k0 == EXPR_BOOL: + saw_bool = 1 + else if k0 == EXPR_ENUMPAT: + saw_enum = 1 + if ast_c(ap0) != 0: + // Payload binding/guard patterns are Task 4/5. + match_mode = 0 + else if k0 == EXPR_IDENT && str_eq(istr_get(ast_int_val(ap0)), "_") != 0: + // The lexer classifies a bare `_` as an identifier because `_` + // is a valid identifier character; the parser's EXPR_WILDCARD + // branch is therefore unreachable for the common form. + wild_total = wild_total + 1 + if ast_c(an0) >= 0: + // A wildcard shadows every later arm; keep the old path so + // the CASE order cannot change which body wins. + match_mode = 0 + else if k0 == EXPR_WILDCARD: + wild_total = wild_total + 1 + if ast_c(an0) >= 0: + match_mode = 0 + else: + match_mode = 0 + an0 = ast_c(an0) + if match_val < 0: + match_mode = 0 + if saw_int != 0 && irv_type(match_val) != TI_INT: + // The backend compares tagged int slots; a different scrutinee form + // (dex, char, struct) needs its own lowering. + match_mode = 0 + if saw_bool != 0 && irv_type(match_val) != TI_BOOL: + match_mode = 0 + if saw_enum != 0 && (saw_int != 0 || saw_bool != 0): + // The backend picks one scrutinee form per MATCH range (enum tag vs + // value slot). Mixed rows would dispatch differently in the + // interpreter, so keep the sequential path instead. + match_mode = 0 + if wild_total > 1: + match_mode = 0 + + // Pass B: only for supported matches. Resolve enum tags and dedupe by + // (kind, value), keeping the first arm. + vals := alloc((arm_total + 1) * 16) // unique cases: kind@0, value@8 + cis := alloc((arm_total + 1) * 8) // arm -> unique-case index, -1 = duplicate + unique_n : ., mut = 0 + arm_scan : ., mut = 0 + if match_mode != 0: + an0 = first_arm + loop: + if an0 < 0: + break + ap0 := ast_a(an0) + k0 := ast_kind(ap0) + kind0 : ., mut = -1 + value0 : ., mut = 0 + if k0 == EXPR_INT: + kind0 = CCR_CASE_KIND_VALUE + value0 = ast_int_val(ap0) + else if k0 == EXPR_BOOL: + bv0 : ., mut = 0 + if ast_int_val(ap0) != 0: + bv0 = 1 + kind0 = CCR_CASE_KIND_VALUE + value0 = bv0 + else if k0 == EXPR_ENUMPAT: + kind0 = CCR_CASE_KIND_ENUM + value0 = get_variant_name_idx(ast_a(ap0)) + if value0 < 0: + match_mode = 0 + else: + kind0 = CCR_CASE_KIND_DEFAULT + value0 = 0 + ci0 : ., mut = -1 + if match_mode != 0: + j0 : ., mut = 0 + dup0 : ., mut = 0 + loop: + if j0 >= unique_n: + break + if cell_load(vals, j0 * 16) == kind0: + if cell_load(vals, j0 * 16 + 8) == value0: + dup0 = 1 + break + j0 = j0 + 1 + if dup0 == 0: + cell_store(vals, unique_n * 16, kind0) + cell_store(vals, unique_n * 16 + 8, value0) + ci0 = unique_n + unique_n = unique_n + 1 + cell_store(cis, arm_scan * 8, ci0) + arm_scan = arm_scan + 1 + an0 = ast_c(an0) + case_base : ., mut = -1 + if match_mode != 0: + case_base = g_lat_case_count + // type_kind 0: MATCH is not a type-face op. The NOD semantic + // type_id is filled by ccr_nod_type_domain from the scrutinee var, + // so the wire records the scrutinee domain instead of an invented + // result domain. + emit(IR_MATCH, -1, match_val, case_base, unique_n, 0) + g_cir_has_match = 1 + // Reserve the whole model range before emitting bodies: nested + // matches in an arm must append after this range, not splice into it. + cj0 : ., mut = 0 + loop: + if cj0 >= unique_n: + break + lat_case_append(cell_load(vals, cj0 * 16 + 8), cell_load(vals, cj0 * 16), -1, -1, 0) + cj0 = cj0 + 1 an : ., mut = first_arm + arm_idx : ., mut = 0 loop: if an < 0: break @@ -2706,6 +3034,34 @@ fn gen_expr(node: int) -> int: is_wildcard := 0 if pat_kind == EXPR_WILDCARD: is_wildcard = 1 + // A binding ident is irrefutable: `x` (or `_` if the lexer ever routes + // it here) matches every value and binds the scrutinee. `_` is a + // wildcard; every other name is a binding arm. + is_ident_bind := 0 + ident_ni : ., mut = -1 + if pat_kind == EXPR_IDENT: + ident_ni = ast_int_val(arm_pat) + if str_eq(istr_get(ident_ni), "_") != 0: + is_wildcard = 1 + else: + is_ident_bind = 1 + // Nested refutable sub-patterns (string/char/int/... inside an enum + // payload) are Task 5b. Do not silently treat the outer variant as an + // unconditional match: route the arm to the fall edge instead. The + // scan must cover the full payload subtree: composite sub-patterns + // are not contiguous from ast_b (e.g. A(x, X(y))). + nested_bad : ., mut = 0 + if pat_kind == EXPR_ENUMPAT: + if sh_match_payload_bad(arm_pat) >= 0: + nested_bad = 1 + if match_mode != 0: + // MATCH owns the dispatch: suppress the sequential compare chain + // and the payload-binding reads (sub-patterns already forced the + // fallback path). + pat_kind = -2 + is_wildcard = 0 + is_ident_bind = 0 + nested_bad = 0 // 可选表示位:臂条件与臂体绑定共用(声明在臂作用域) rep_v : ., mut = -1 body_lbl := new_label() @@ -2713,55 +3069,167 @@ fn gen_expr(node: int) -> int: has_next := 0 if ast_c(an) >= 0: has_next = 1 - if is_wildcard == 1: + if is_wildcard == 1 || is_ident_bind == 1: emit(IR_JUMP, -1, body_lbl, 0, 0, 0) else if pat_kind == EXPR_ENUMPAT: - variant_ni := get_variant_name_idx(ast_a(arm_pat)) - // 解包侧判表示,scrutinee 配表示位(可选槽)时按位分派: - // 裸值(0)-> 值即携带值:Some 模式命中臂体(不读 tag,裸值非对象, - // 读 tag 即 SIGSEGV = 原红态);None 模式不命中(裸值非 null)。 - // 装箱(≠0)-> 既有 tag 分派(逐字不变)。 - // 无表示位(非可选槽 / 未覆盖情形)-> 既有路径逐字不变(装箱假定)。 - if g_optrep_on != 0 && match_val >= 0: - rep_v = irv_rep(match_val) - if rep_v < 0: - tag_var := new_ir_var("tag", TI_INT) - emit(IR_LOAD_ENUM_TAG, tag_var, match_val, 0, 0, 0) - vtag := new_ir_var("vtag", TI_INT) - emit(IR_CONST, vtag, variant_ni, 0, 0, TI_INT) - cmp_var := new_ir_var("cmp", TI_INT) - emit(IR_BINARY, cmp_var, tag_var, vtag, OP_EQ, 0) + if nested_bad != 0: if has_next == 1: fall_lbl = new_label() - emit(IR_BRANCH, -1, cmp_var, body_lbl, fall_lbl, 0) + emit(IR_JUMP, -1, fall_lbl, 0, 0, 0) else: - is_some_pat : ., mut = 0 - if str_eq(istr_get(variant_ni), "Some") != 0: - is_some_pat = 1 - tag_lbl := new_label() - if has_next == 1: - fall_lbl = new_label() - zb := new_ir_var("_rz", TI_INT) - emit(IR_CONST, zb, 0, 0, 0, TI_INT) - isb := new_ir_var("rep_isbare", TI_INT) - if is_some_pat != 0: - emit(IR_BINARY, isb, rep_v, zb, OP_EQ, 0) - else: - emit(IR_BINARY, isb, rep_v, zb, OP_NE, 0) - // Some:裸 -> 直接命中;装箱 -> 落 tag 比对。None:裸 -> 不命中(落 fall); - // 装箱 -> 落 tag 比对。 - if is_some_pat != 0: - emit(IR_BRANCH, -1, isb, body_lbl, tag_lbl, 0) + variant_ni := get_variant_name_idx(ast_a(arm_pat)) + // 解包侧判表示,scrutinee 配表示位(可选槽)时按位分派: + // 裸值(0)-> 值即携带值:Some 模式命中臂体(不读 tag,裸值非对象, + // 读 tag 即 SIGSEGV = 原红态);None 模式不命中(裸值非 null)。 + // 装箱(≠0)-> 既有 tag 分派(逐字不变)。 + // 无表示位(非可选槽 / 未覆盖情形)-> 既有路径逐字不变(装箱假定)。 + if g_optrep_on != 0 && match_val >= 0: + rep_v = irv_rep(match_val) + if rep_v < 0: + tag_var := new_ir_var("tag", TI_INT) + emit(IR_LOAD_ENUM_TAG, tag_var, match_val, 0, 0, 0) + vtag := new_ir_var("vtag", TI_INT) + emit(IR_CONST, vtag, variant_ni, 0, 0, TI_INT) + cmp_var := new_ir_var("cmp", TI_INT) + emit(IR_BINARY, cmp_var, tag_var, vtag, OP_EQ, 0) + if has_next == 1: + fall_lbl = new_label() + emit(IR_BRANCH, -1, cmp_var, body_lbl, fall_lbl, 0) else: - emit(IR_BRANCH, -1, isb, tag_lbl, fall_lbl, 0) - emit(IR_LABEL, -1, tag_lbl, 0, 0, 0) - tag_var := new_ir_var("tag", TI_INT) - emit(IR_LOAD_ENUM_TAG, tag_var, match_val, 0, 0, 0) - vtag := new_ir_var("vtag", TI_INT) - emit(IR_CONST, vtag, variant_ni, 0, 0, TI_INT) - cmp_var := new_ir_var("cmp", TI_INT) - emit(IR_BINARY, cmp_var, tag_var, vtag, OP_EQ, 0) - emit(IR_BRANCH, -1, cmp_var, body_lbl, fall_lbl, 0) + is_some_pat : ., mut = 0 + if str_eq(istr_get(variant_ni), "Some") != 0: + is_some_pat = 1 + tag_lbl := new_label() + if has_next == 1: + fall_lbl = new_label() + zb := new_ir_var("_rz", TI_INT) + emit(IR_CONST, zb, 0, 0, 0, TI_INT) + isb := new_ir_var("rep_isbare", TI_INT) + if is_some_pat != 0: + emit(IR_BINARY, isb, rep_v, zb, OP_EQ, 0) + else: + emit(IR_BINARY, isb, rep_v, zb, OP_NE, 0) + // Some:裸 -> 直接命中;装箱 -> 落 tag 比对。None:裸 -> 不命中(落 fall); + // 装箱 -> 落 tag 比对。 + if is_some_pat != 0: + emit(IR_BRANCH, -1, isb, body_lbl, tag_lbl, 0) + else: + emit(IR_BRANCH, -1, isb, tag_lbl, fall_lbl, 0) + emit(IR_LABEL, -1, tag_lbl, 0, 0, 0) + tag_var := new_ir_var("tag", TI_INT) + emit(IR_LOAD_ENUM_TAG, tag_var, match_val, 0, 0, 0) + vtag := new_ir_var("vtag", TI_INT) + emit(IR_CONST, vtag, variant_ni, 0, 0, TI_INT) + cmp_var := new_ir_var("cmp", TI_INT) + emit(IR_BINARY, cmp_var, tag_var, vtag, OP_EQ, 0) + emit(IR_BRANCH, -1, cmp_var, body_lbl, fall_lbl, 0) + else if pat_kind == EXPR_STRUCTPAT: + // Struct pattern: sequential field tests. The lowerer never + // classifies struct patterns as MATCH, so this is the only path. + // A missing struct/field or an unsupported nested subpattern + // fails closed: jump to the fall edge, never to the body. + si := find_struct_by_name(ast_a(arm_pat)) + sc := ast_c(arm_pat) + w0 := ast_b(arm_pat) + can_match : ., mut = 1 + if si < 0 || w0 < 0: + can_match = 0 + if has_next == 1: + fall_lbl = new_label() + si_i : ., mut = 0 + loop: + if si_i >= sc: + break + w := w0 + si_i + sub : ., mut = -1 + fni : ., mut = -1 + if w >= 0: + sub = ast_a(w) + fni = ast_b(w) + jdi : ., mut = -1 + if si >= 0: + jdi = struct_field_index_by_name(si, fni) + if jdi < 0: + can_match = 0 + break + if sub >= 0: + sk := ast_kind(sub) + if sk != EXPR_IDENT && sk != EXPR_WILDCARD: + fd := ast_data(w) + ft : ., mut = struct_row_field_ti(si, jdi) + if fd > 0: + ft = fd - 1 + if ti_has_generic_param(ft) != 0: + // The checker should have rejected this before + // codegen. Never emit a field load with an + // unresolved type: fail closed. + check_error(EC_TS_FIELD_TYPE, + "Struct pattern field has an unresolved generic type", + ast_line(sub), ast_col(sub)) + can_match = 0 + break + fv := new_ir_var("fld", TI_INT) + irv_set_type(fv, structpat_field_load_ti_from(ft)) + emit(IR_LOAD_FIELD, fv, match_val, 0, jdi, 0) + if gen_structpat_field_test(sub, fv, fall_lbl) < 0: + can_match = 0 + break + si_i = si_i + 1 + if can_match != 0: + emit(IR_JUMP, -1, body_lbl, 0, 0, 0) + else: + emit(IR_JUMP, -1, fall_lbl, 0, 0, 0) + else if pat_kind == EXPR_TUPLEPAT: + // Tuple pattern: sequential element tests. The checker has already + // validated the scrutinee arity and cached each element type on the + // wrapper. A missing wrapper/type or an unsupported subpattern fails + // closed: jump to the fall edge, never to the body. + tc := ast_b(arm_pat) + tw0 := ast_a(arm_pat) + can_match : ., mut = 1 + if tw0 < 0 || tc <= 0: + can_match = 0 + if has_next == 1: + fall_lbl = new_label() + ti_i : ., mut = 0 + loop: + if ti_i >= tc: + break + w := tw0 + ti_i + sub : ., mut = -1 + idx : ., mut = -1 + fd : ., mut = 0 + if w >= 0: + sub = ast_a(w) + idx = ast_b(w) + fd = ast_data(w) + if w < 0 || idx != ti_i || fd <= 0: + can_match = 0 + break + sk : ., mut = -1 + if sub >= 0: + sk = ast_kind(sub) + if sk == EXPR_IDENT || sk == EXPR_WILDCARD: + ti_i = ti_i + 1 + continue + if sk != EXPR_INT && sk != EXPR_BOOL && sk != EXPR_CHAR && sk != EXPR_STRING: + can_match = 0 + break + ft := fd - 1 + if ti_has_generic_param(ft) != 0: + can_match = 0 + break + fv := new_ir_var("fld", TI_INT) + irv_set_type(fv, structpat_field_load_ti_from(ft)) + emit(IR_LOAD_FIELD, fv, match_val, 0, idx, 0) + if gen_structpat_field_test(sub, fv, fall_lbl) < 0: + can_match = 0 + break + ti_i = ti_i + 1 + if can_match != 0: + emit(IR_JUMP, -1, body_lbl, 0, 0, 0) + else: + emit(IR_JUMP, -1, fall_lbl, 0, 0, 0) else if pat_kind == EXPR_INT: pat_val := new_ir_var("pval", TI_INT) emit(IR_CONST, pat_val, ast_int_val(arm_pat), 0, 0, TI_INT) @@ -2781,8 +3249,51 @@ fn gen_expr(node: int) -> int: if has_next == 1: fall_lbl = new_label() emit(IR_BRANCH, -1, cmp_var, body_lbl, fall_lbl, 0) + else if pat_kind == EXPR_CHAR: + // char 运行期值 = 字符驻留串索引;模式存同一索引(ast_int_val)。 + // 直接比较当前值,不改 char 语义(不是 Unicode 码点)。 + pat_val := new_ir_var("pval", TI_CHAR) + track_str(ast_int_val(arm_pat)) + emit(IR_CONST, pat_val, ast_int_val(arm_pat), 0, 0, TI_CHAR) + cmp_var := new_ir_var("cmp", TI_INT) + emit(IR_BINARY, cmp_var, match_val, pat_val, OP_EQ, 0) + if has_next == 1: + fall_lbl = new_label() + emit(IR_BRANCH, -1, cmp_var, body_lbl, fall_lbl, 0) + else if pat_kind == EXPR_STRING: + // String patterns do not enter CASE: compare through str_eq and + // branch on the boolean result (the sequential path). + pat_val := new_ir_var("spat", TI_STR) + track_str(ast_int_val(arm_pat)) + emit(IR_CONST, pat_val, ast_int_val(arm_pat), 0, 0, TI_STR) + eq0 := new_ir_var("_seq0", TI_STR) + eq1 := new_ir_var("_seq1", TI_STR) + emit(IR_STORE, -1, eq0, match_val, 0, 0) + emit(IR_STORE, -1, eq1, pat_val, 0, 0) + cmp_var := new_ir_var("cmp", TI_BOOL) + emit(IR_CALL, cmp_var, eq0, 2, str_intern("str_eq"), TI_BOOL) + if has_next == 1: + fall_lbl = new_label() + emit(IR_BRANCH, -1, cmp_var, body_lbl, fall_lbl, 0) + else: + // Unsupported pattern kind (struct/tuple/interpolated string/bigint). + // Preserve refutability: never fall through to the body. + if match_mode == 0: + if has_next == 1: + fall_lbl = new_label() + emit(IR_JUMP, -1, fall_lbl, 0, 0, 0) emit(IR_LABEL, -1, body_lbl, 0, 0, 0) + if match_mode != 0: + ci_arm := cell_load(cis, arm_idx * 8) + if ci_arm >= 0: + // The semantic model stores the body label number. ccr_lower + // resolves it to the NOD id when it derives the wire table. + lat_case_set_target(case_base + ci_arm, body_lbl) push_ir_scope() + if pat_kind == EXPR_STRUCTPAT: + gen_structpat_binds(arm_pat, match_val) + if pat_kind == EXPR_TUPLEPAT: + gen_tuplepat_binds(arm_pat, match_val) if pat_kind == EXPR_ENUMPAT: sub_count := ast_c(arm_pat) fi : ., mut = 0 @@ -2833,6 +3344,9 @@ fn gen_expr(node: int) -> int: if spn >= 0 && ast_kind(spn) == EXPR_IDENT: bind_local(ast_int_val(spn), fv) fi = fi + 1 + if is_ident_bind == 1: + // Irrefutable binding arm: the name is the scrutinee itself. + bind_local(ident_ni, match_val) body_val := gen_expr(arm_body) body_val = force_if_thunk(body_val) if body_val >= 0: @@ -2842,10 +3356,10 @@ fn gen_expr(node: int) -> int: emit_rep_set(mrep_var, rep_enc_of_expr(arm_body, body_val)) pop_ir_scope() emit(IR_JUMP, -1, merge_lbl, 0, 0, 0) - if is_wildcard == 0: - if has_next == 1: - emit(IR_LABEL, -1, fall_lbl, 0, 0, 0) + if match_mode == 0 && is_wildcard == 0 && is_ident_bind == 0 && has_next == 1: + emit(IR_LABEL, -1, fall_lbl, 0, 0, 0) an = ast_c(an) + arm_idx = arm_idx + 1 emit(IR_LABEL, -1, merge_lbl, 0, 0, 0) return result_var @@ -3314,6 +3828,8 @@ fn gen_expr(node: int) -> int: return inner if ast_kind(node) == EXPR_STRUCTPAT: return -1 + if ast_kind(node) == EXPR_TUPLEPAT: + return -1 if ast_kind(node) == EXPR_STMT: gen_expr(ast_a(node)) return -1 @@ -3353,6 +3869,7 @@ fn ir_gen_func(fi: int): // Skip generic functions — they will be monomorphized at call sites if fi_generic_count(fi) > 0: return + g_cir_has_match = 0 fn_node := fi_ast_node(fi) name_idx := ast_a(fn_node) first_param := ast_b(fn_node) @@ -4191,6 +4708,15 @@ fn ir_gen_all(): g_next_label = 1 g_ir_loop_depth = 0 g_ir_str_const_count = 0 + g_lat_case = "" + g_lat_case_count = 0 + g_lat_case_cap = 0 + // The wire table is derived by ccr_lower; reset it too so a second IR run + // in one process cannot inherit stale rows. + g_ccr_case = "" + g_ccr_case_count = 0 + g_ccr_case_cap = 0 + g_cir_has_match = 0 // 可选表示方式:侧表复位 + 启用扫描(先于全局注册,隐藏信道的 var 记录顺序依赖) optrep_begin() diff --git a/src/compiler/main.cr b/src/compiler/main.cr index 26b88b0c..dde6d2e5 100644 --- a/src/compiler/main.cr +++ b/src/compiler/main.cr @@ -238,6 +238,7 @@ fn corec_main() -> int: cli_flag_bool_debug("dump-types", "", "Debug: dump TYPE segment") cli_flag_bool_debug("dump-ifaces", "", "Debug: dump IFACE segment") cli_flag_bool_debug("dump-tk-terms", "", "Debug: dump per-DFNode type terms") + cli_flag_bool_debug("dump-case-model", "", "Debug: report semantic CASE model vs derived wire counts") // The old type-shadow CLI channels were removed; the judgment path has no // switch and always goes through the engine. cli_flag_bool_debug("verify-named-dedup", "", "Debug: verify type side-table consistency") @@ -388,7 +389,14 @@ fn corec_main() -> int: run_rc := run_frontend() if run_rc != 0: return 1 + ir_diag_base_run := g_diag_count ir_gen_all() + if g_diag_count > ir_diag_base_run: + print_diagnostics() + return 1 + // The interpreter consumes the wire CASE table. Derive it from the + // semantic model exactly as the .ccr path does before executing. + lower_to_ccr() return ir_interpret() // === clean-cache: delete incremental compilation cache === @@ -451,6 +459,14 @@ fn corec_main() -> int: g_ir_str_const_count = 0 g_ir_source_hash = 0 g_ir_source_hash_ready = 0 + g_lat_case = "" + g_lat_case_count = 0 + g_lat_case_cap = 0 + // Wire CASE is derived by ccr_lower; clear both to avoid stale rows. + g_ccr_case = "" + g_ccr_case_count = 0 + g_ccr_case_cap = 0 + g_cir_has_match = 0 init_df() // This path does not use ir_gen_all, so initialize optional representation // state here, before ir_gen_globals, because the hidden channels depend on @@ -483,6 +499,7 @@ fn corec_main() -> int: g_cir_skip_journal = 1 // Generate IR for each function, checking cache first + ir_diag_base := g_diag_count fi : ., mut = 0 loop: if fi >= g_func_count: @@ -564,17 +581,36 @@ fn corec_main() -> int: df_end_func(ir_func_idx) // Save only when generation did not change snapshot-uncarried shared state. + // MATCH CASE model rows are part of the snapshot (v25), so a MATCH + // function is saved like any other. g_cir_has_match remains + // diagnostic only. if cache_enabled != 0: if g_type_count == tc0: save_cir_cache(cache_path, fi, ir_func_idx) fi = fi + 1 + // IR generation may add diagnostics (for example, monomorph instance + // revalidation). Previously they were only printed by the later + // pointer-analysis gate, which takes its base afterwards and therefore could + // not see them. Fail closed on any new IR-generation diagnostic. + if g_diag_count > ir_diag_base: + print_diagnostics() + return 1 + // Finalize purity and the state chain after IR generation and before any chain // consumer. True purity needs the whole program, and chain construction depends // on it. df_state_finalize() + // Hidden contract channel: report the semantic CASE model and the derived + // wire count before lowering. Normal builds print nothing. + if cli_has("dump-case-model") != 0: + print("case_model_before ") + print(int_str(g_lat_case_count)) + print(" ") + println(int_str(g_ccr_case_count)) + // === cir: output dataflow graph === if cli_eq(cmd, "cir"): // Register allocation checks and entry dumps live in corearch now. @@ -615,6 +651,11 @@ fn corec_main() -> int: pass_cse() println("lower to ccr...") lower_to_ccr() + if cli_has("dump-case-model") != 0: + print("case_model_after ") + print(int_str(g_lat_case_count)) + print(" ") + println(int_str(g_ccr_case_count)) print("lower done: ") print(int_str(g_ir_func_count)) print(" funcs, ") diff --git a/src/compiler/monomorph.cr b/src/compiler/monomorph.cr index e3d10228..89577b38 100644 --- a/src/compiler/monomorph.cr +++ b/src/compiler/monomorph.cr @@ -31,6 +31,13 @@ g_gen_dedup_new : string, mut g_gen_dedup_count : int, mut g_gen_dedup_cap : int, mut +// Defensive struct-pattern validation for instances. The checker resolves field +// types on each wrapper (wrapper.data = ti+1); if one still contains an +// unresolved generic parameter after substitution, the clone fails closed. +g_gen_structpat_bad : int, mut +g_gen_structpat_bad_line : int, mut +g_gen_structpat_bad_col : int, mut + // ============================================================ // Grow helpers // ============================================================ @@ -987,26 +994,55 @@ fn gen_clone_tree(node: int) -> int: return n // EXPR_STRUCTPAT: a = name, b = first wrapper (consecutive), c = field count. - // wrapper.a = subpattern node. + // wrapper.a = subpattern node, wrapper.b = field name index (same contract as + // struct literal wrappers). Preserve the name so a clone still binds by name. if k == EXPR_STRUCTPAT: + pbad : ., mut = 0 if b >= 0 && c > 0: nvs : string, mut = alloc(c * 8) + nns : string, mut = alloc(c * 8) + nds : string, mut = alloc(c * 8) i : ., mut = 0 loop: if i >= c: break vn : ., mut = -1 + nn : ., mut = -1 + nd : ., mut = 0 if b + i >= 0: vn = ast_a(b + i) + nn = ast_b(b + i) + nd = ast_data(b + i) // checker-resolved field type + 1 + if nd > 0: + ft := nd - 1 + sk : ., mut = -1 + if vn >= 0: + sk = ast_kind(vn) + // `_` neither compares nor binds, so a still-generic field is + // safe there. ident/literal subpatterns would load/bind it. + if ti_has_generic_param(ft) != 0 && sk != EXPR_WILDCARD: + pbad = 1 + if g_gen_structpat_bad == 0: + g_gen_structpat_bad = 1 + g_gen_structpat_bad_line = ln + g_gen_structpat_bad_col = cl cell_store(nvs, i * 8, gen_clone_tree(vn)) + cell_store(nns, i * 8, nn) + cell_store(nds, i * 8, nd) i = i + 1 new_first := g_ast_count i = 0 loop: if i >= c: break - ast_alloc(EXPR_NONE, cell_load(nvs, i * 8), 0, 0, 0, 0, 0, ln, cl) + ast_alloc(EXPR_NONE, cell_load(nvs, i * 8), cell_load(nns, i * 8), 0, 0, 0, cell_load(nds, i * 8), ln, cl) i = i + 1 + if pbad != 0: + // Fail closed: b = -1 makes ir_gen reject the pattern without + // emitting any field load. + n := ast_alloc(k, a, -1, c, iv, tv, d, ln, cl) + gen_dedup_add(node, n) + return n n := ast_alloc(k, a, new_first, c, iv, tv, d, ln, cl) gen_dedup_add(node, n) return n @@ -1014,6 +1050,44 @@ fn gen_clone_tree(node: int) -> int: gen_dedup_add(node, n) return n + // EXPR_TUPLEPAT: a = first wrapper (consecutive), b = element count. + // wrapper.a = subpattern node, wrapper.b = element index, wrapper.d = + // checker-resolved element type + 1. Preserve both halves of the wrapper so a + // clone still compares/binds the same positions with the same type. + if k == EXPR_TUPLEPAT: + if a >= 0 && b > 0: + nvs : string, mut = alloc(b * 8) + nns : string, mut = alloc(b * 8) + nds : string, mut = alloc(b * 8) + i : ., mut = 0 + loop: + if i >= b: + break + vn : ., mut = -1 + nn : ., mut = -1 + nd : ., mut = 0 + if a + i >= 0: + vn = ast_a(a + i) + nn = ast_b(a + i) + nd = ast_data(a + i) + cell_store(nvs, i * 8, gen_clone_tree(vn)) + cell_store(nns, i * 8, nn) + cell_store(nds, i * 8, nd) + i = i + 1 + new_first := g_ast_count + i = 0 + loop: + if i >= b: + break + ast_alloc(EXPR_NONE, cell_load(nvs, i * 8), cell_load(nns, i * 8), 0, 0, 0, cell_load(nds, i * 8), ln, cl) + i = i + 1 + n := ast_alloc(k, new_first, b, c, iv, tv, d, ln, cl) + gen_dedup_add(node, n) + return n + n := ast_alloc(k, a, b, c, iv, tv, d, ln, cl) + gen_dedup_add(node, n) + return n + // ── EXPR_AT: a=name_ni(NOT), b=args_node(YES or -1) ── if k == EXPR_AT: b2 := gen_clone_tree(b) @@ -1096,7 +1170,14 @@ fn gen_create_instance(func_ni: int, type_args: string, bstart: int) -> int: orig_fn_node := fi_ast_node(func_ni) g_gen_dedup_count = 0 g_gen_dedup_cap = 0 + g_gen_structpat_bad = 0 + g_gen_structpat_bad_line = 0 + g_gen_structpat_bad_col = 0 new_fn_node := gen_clone_tree(orig_fn_node) + if g_gen_structpat_bad != 0: + check_error(EC_TS_FIELD_TYPE, + "Struct pattern field has an unresolved generic type", + g_gen_structpat_bad_line, g_gen_structpat_bad_col) // Concretize the instance parameter chain. The EXPR_FN clone branch copies b/c // (first parameter and count), so the instance would share the declaration's diff --git a/src/compiler/opt.cr b/src/compiler/opt.cr index 4586efb3..9cd9f67c 100644 --- a/src/compiler/opt.cr +++ b/src/compiler/opt.cr @@ -384,10 +384,12 @@ fn pass_cse(): new_v := cell_load(replace_map, rj * 8 + 8) if iri_s1(inst) == old_v: iri_set_s1(inst, new_v) - if iri_s2(inst) == old_v: - iri_set_s2(inst, new_v) - if iri_s3(inst) == old_v: - iri_set_s3(inst, new_v) + // MATCH s2/s3 are CASE start/count, not variables. + if op != IR_MATCH: + if iri_s2(inst) == old_v: + iri_set_s2(inst, new_v) + if iri_s3(inst) == old_v: + iri_set_s3(inst, new_v) rj = rj + 1 ii = ii + 1 diff --git a/src/compiler/parser.cr b/src/compiler/parser.cr index 7a93b56e..68666cce 100644 --- a/src/compiler/parser.cr +++ b/src/compiler/parser.cr @@ -1412,6 +1412,27 @@ fn parse_match_expr() -> int: g_want_value = want return alloc_node(EXPR_MATCH, expr, af, ac, 0, 0, 0, tok_ln(t), tok_cl(t)) +// Skip the remainder of a malformed tuple pattern through its matching ')'. +// The caller has already emitted P005; recovery only prevents cascading +// diagnostics from the tokens the malformed element left behind. +fn tuplepat_recover(): + depth : ., mut = 0 + loop: + k := tok_k(cur_tok()) + if k == T_EOF: + break + if k == T_LPAREN: + depth = depth + 1 + advance_tok() + continue + if k == T_RPAREN: + advance_tok() + if depth == 0: + break + depth = depth - 1 + continue + advance_tok() + fn parse_pattern() -> int: t := cur_tok() if tok_k(t) == T_UNDERSCORE: @@ -1424,6 +1445,107 @@ fn parse_pattern() -> int: advance_tok() tl := cell_load(g_tokens, t * ESZ_TOKEN + OFF_TK_LEXEME) return alloc_node(EXPR_BIGINT, tl, 0, 0, 0, TY_INT, 0, tok_ln(t), tok_cl(t)) + if tok_k(t) == T_LPAREN: + // Tuple pattern: (pat, pat, ...). EXPR_TUPLEPAT layout: a = first wrapper + // (consecutive), b = element count. wrapper.a = subpattern, wrapper.b = + // element index (0-based). The contract is the same as EXPR_STRUCTPAT, + // except the second wrapper slot is positional instead of a field name. + // Composite subpatterns occupy several nodes, so parse all subpatterns + // first, then build contiguous wrappers. `(pat)` is grouping, exactly like + // a parenthesized expression. Empty `()`, leading commas, and trailing + // commas are hard parser diagnostics. An element that parse_pattern could + // not parse (its fallback returns 0) is diagnosed here and never stored: + // a node 0 must not reach the checker or IR as a pattern. + advance_tok() + if check(T_RPAREN): + check_error(EC_P_PATTERN, + "Empty tuple pattern is not supported; use '_'", + tok_ln(cur_tok()), tok_cl(cur_tok())) + advance_tok() + return alloc_node(EXPR_WILDCARD, 0, 0, 0, 0, 0, 0, tok_ln(t), tok_cl(t)) + if check(T_COMMA): + check_error(EC_P_PATTERN, "Unexpected token in pattern", + tok_ln(cur_tok()), tok_cl(cur_tok())) + tuplepat_recover() + return alloc_node(EXPR_WILDCARD, 0, 0, 0, 0, 0, 0, tok_ln(t), tok_cl(t)) + cap : ., mut = 8 + vals : string, mut = alloc(cap * 8) + et := cur_tok() + e0 := parse_pattern() + if e0 <= 0: + check_error(EC_P_PATTERN, "Unexpected token in pattern", + tok_ln(et), tok_cl(et)) + tuplepat_recover() + return alloc_node(EXPR_WILDCARD, 0, 0, 0, 0, 0, 0, tok_ln(t), tok_cl(t)) + cell_store(vals, 0, e0) + pc : ., mut = 1 + loop: + if check(T_COMMA): + advance_tok() // consume comma + if check(T_RPAREN): + check_error(EC_P_PATTERN, + "Trailing comma is not supported in a tuple pattern", + tok_ln(cur_tok()), tok_cl(cur_tok())) + advance_tok() + return alloc_node(EXPR_WILDCARD, 0, 0, 0, 0, 0, 0, tok_ln(t), tok_cl(t)) + if check(T_COMMA): + check_error(EC_P_PATTERN, "Unexpected token in pattern", + tok_ln(cur_tok()), tok_cl(cur_tok())) + tuplepat_recover() + return alloc_node(EXPR_WILDCARD, 0, 0, 0, 0, 0, 0, tok_ln(t), tok_cl(t)) + if check(T_EOF): + check_error(EC_P_BRACKET, + "Missing closing ')' in tuple pattern", + tok_ln(cur_tok()), tok_cl(cur_tok())) + return alloc_node(EXPR_WILDCARD, 0, 0, 0, 0, 0, 0, tok_ln(t), tok_cl(t)) + et2 := cur_tok() + en := parse_pattern() + if en <= 0: + check_error(EC_P_PATTERN, "Unexpected token in pattern", + tok_ln(et2), tok_cl(et2)) + tuplepat_recover() + return alloc_node(EXPR_WILDCARD, 0, 0, 0, 0, 0, 0, tok_ln(t), tok_cl(t)) + if pc >= cap: + ncap := cap * 2 + nv := alloc(ncap * 8) + cell_copy(vals, cap * 8, nv) + vals = nv + cap = ncap + cell_store(vals, pc * 8, en) + pc = pc + 1 + continue + if check(T_RPAREN): + advance_tok() + break + if check(T_EOF): + check_error(EC_P_BRACKET, + "Missing closing ')' in tuple pattern", + tok_ln(cur_tok()), tok_cl(cur_tok())) + return alloc_node(EXPR_WILDCARD, 0, 0, 0, 0, 0, 0, tok_ln(t), tok_cl(t)) + // Missing separator between elements. + check_error(EC_P_PATTERN, "Unexpected token in pattern", + tok_ln(cur_tok()), tok_cl(cur_tok())) + tuplepat_recover() + return alloc_node(EXPR_WILDCARD, 0, 0, 0, 0, 0, 0, tok_ln(t), tok_cl(t)) + if pc == 1: + // Parenthesized pattern: transparent grouping, same as `(expr)`. + return cell_load(vals, 0) + pf : ., mut = -1 + pi : ., mut = 0 + loop: + if pi >= pc: + break + vn := cell_load(vals, pi * 8) + ln : ., mut = tok_ln(t) + cl : ., mut = tok_cl(t) + if vn >= 0: + ln = ast_line(vn) + cl = ast_col(vn) + wl := ast_alloc(EXPR_NONE, vn, pi, 0, 0, 0, 0, ln, cl) + if pi == 0: + pf = wl + pi = pi + 1 + return alloc_node(EXPR_TUPLEPAT, pf, pc, 0, 0, 0, 0, tok_ln(t), tok_cl(t)) if tok_k(t) == T_STRING || tok_k(t) == T_INTERP: return parse_interp_run() if tok_k(t) == T_CHAR: @@ -1487,12 +1609,14 @@ fn parse_pattern() -> int: if check(T_LBRACE): // Struct pattern: Name { field = pat, ... } // EXPR_STRUCTPAT layout: a = name index, b = first wrapper, c = field - // count. Wrappers are contiguous; wrapper.a points to the subpattern. + // count. Wrappers are contiguous; wrapper.a points to the subpattern and + // wrapper.b is the field name index (same contract as struct literals). // Composite subpatterns occupy several nodes, so parse all subpatterns // first, then build the wrappers. advance_tok() cap : ., mut = 8 vals : string, mut = alloc(cap * 8) + names : string, mut = alloc(cap * 8) fc : ., mut = 0 loop: if check(T_RBRACE) || check(T_EOF): @@ -1511,8 +1635,12 @@ fn parse_pattern() -> int: nv := alloc(ncap * 8) cell_copy(vals, cap * 8, nv) vals = nv + nn := alloc(ncap * 8) + cell_copy(names, cap * 8, nn) + names = nn cap = ncap cell_store(vals, fc * 8, parse_pattern()) + cell_store(names, fc * 8, fni) fc = fc + 1 if check(T_COMMA): advance_tok() @@ -1528,7 +1656,7 @@ fn parse_pattern() -> int: if vn >= 0: ln = ast_line(vn) cl = ast_col(vn) - wl := ast_alloc(EXPR_NONE, vn, 0, 0, 0, 0, 0, ln, cl) + wl := ast_alloc(EXPR_NONE, vn, cell_load(names, fi2 * 8), 0, 0, 0, 0, ln, cl) if fi2 == 0: ff = wl fi2 = fi2 + 1 diff --git a/src/compiler/ty_shadow.cr b/src/compiler/ty_shadow.cr index e1fdd23e..3f70f8f9 100644 --- a/src/compiler/ty_shadow.cr +++ b/src/compiler/ty_shadow.cr @@ -1214,7 +1214,93 @@ fn sh_match_pat_kind(pat_node: int) -> int: // 绑定 = 通配(值域不绑;与 ir_gen 的按值比较臂不同层) if k == EXPR_ENUMPAT: return 2 - return 0 // 字面量 / struct 模式 / EXPR_NONE -> 不可映射(登记) + return 0 // 字面量 / struct 模式 / tuple 模式 / EXPR_NONE -> 不可映射(登记) + +// 字面量模式种类(int/bool/string/char),-1 = 非字面量模式。 +// 这些模式在 checker 侧可逐值比较,但域是无限的(bool 除外),因此没有 +// `_`/绑定臂时一律按不穷尽处理;枚举域仍走类型引擎。 +fn sh_match_lit_kind(pat_node: int) -> int: + if pat_node < 0: + return -1 + k := ast_kind(pat_node) + if k == EXPR_INT || k == EXPR_BOOL || k == EXPR_STRING || k == EXPR_CHAR: + return k + return -1 + +// 同一字面量(kind + value)是否已在前面的臂出现(首个匹配语义)。 +fn sh_match_lit_seen(first_arm: int, cur_arm: int, kind: int, value: int) -> int: + an := first_arm + loop: + if an < 0 || an == cur_arm: + break + p := ast_a(an) + if p >= 0: + if ast_kind(p) == kind && ast_int_val(p) == value: + return 1 + an = ast_c(an) + return 0 + +// 模式子树的最小节点下标。parser 先分配子模式节点、最后分配外层模式节点; +// 复合子模式(X(y))的根下标高于自己的后代,因此 ast_b 不是子树起点,必须递归到 +// 第一个子模式(parser.cr parse_pattern 的分配序:子先父后)。 +fn sh_match_pat_subtree_start(p: int) -> int: + if p < 0: + return -1 + k := ast_kind(p) + if k == EXPR_ENUMPAT: + if ast_c(p) > 0: + c := ast_b(p) + if c >= 0 && c < p: + s := sh_match_pat_subtree_start(c) + if s >= 0 && s < p: + return s + else if k == EXPR_STRUCTPAT: + if ast_c(p) > 0: + w := ast_b(p) + if w >= 0 && w < p: + c := ast_a(w) + if c >= 0 && c < p: + s := sh_match_pat_subtree_start(c) + if s >= 0 && s < p: + return s + else if k == EXPR_TUPLEPAT: + // Tuple patterns use the array/tuple wrapper contract: a=first wrapper, + // b=element count, wrapper.a=subpattern. + if ast_b(p) > 0: + w := ast_a(p) + if w >= 0 && w < p: + c := ast_a(w) + if c >= 0 && c < p: + s := sh_match_pat_subtree_start(c) + if s >= 0 && s < p: + return s + return p + +// Enum payload 内的首个 refutable 节点(-1 = 全部为 ident/wildcard)。 +// 扫描区间 = [首个子模式子树起点, pat_node)。parser 在分配外层节点前连续分配 +// 完整 payload 子树,因此该区间覆盖所有子模式,包括“复合子模式不在首位”的 +// A(x, X(y)) —— 只按 ast_b + i 会漏掉后面的复合子模式。 +fn sh_match_payload_bad(pat_node: int) -> int: + if pat_node < 0: + return -1 + count := ast_c(pat_node) + if count <= 0: + return -1 + first := ast_b(pat_node) + if first < 0 || first >= pat_node: + return pat_node + start := sh_match_pat_subtree_start(first) + if start < 0 || start >= pat_node: + return pat_node + i := start + loop: + if i >= pat_node: + return -1 + k := ast_kind(i) + if k != EXPR_IDENT && k != EXPR_WILDCARD: + return i + i = i + 1 + return -1 // 变体模式 -> 本枚举变体下标(-1 = 不属本枚举 / 名字不可解析 / scrutinee 不可展开)。 // 名字有两种形态(parser.cr parse_pattern 的产物):裸变体名 Red,或限定名 diff --git a/src/format/elf/elf.cr b/src/format/elf/elf.cr index d9f2adb4..42c17560 100644 --- a/src/format/elf/elf.cr +++ b/src/format/elf/elf.cr @@ -1798,6 +1798,22 @@ fn elf_gen(buf: string) -> int: g_x86_rodataref_count = 0 g_x86_alloc_patch_count = 0 g_x86_ext_rel_count = 0 + // Per-run emission counter: reset before the sizing-result Phase 3 pass so + // it counts real emission only, not the Phase 2 sizing calls. + g_match_bin_emits = 0 + g_match_table_emits = 0 + g_match_table_cursor = 0 + g_match_table_ref_count = 0 + // Global label -> absolute position map for the rodata table writer. g_label_poses + // is overwritten per function; table targets are global label numbers. + if g_label_abs_cap < g_label_count: + grow_label_abs(g_label_count) + lab_abs_i : ., mut = 0 + loop: + if lab_abs_i >= g_label_count: + break + cell_store(g_label_abs, lab_abs_i * 8, -1) + lab_abs_i = lab_abs_i + 1 fi = 0 loop: if fi >= g_ir_func_count: @@ -2239,6 +2255,12 @@ fn elf_gen(buf: string) -> int: cp = cp + 1 si = si + 1 + // Dense MATCH jump tables: after strings, before the HIT pool, 4-aligned. + // The writer rescans functions in emission order with the same predicate, so + // each table lands at the in-rodata offset recorded by its LEA reference. + // Strings end 8-aligned, so no pad is needed here. + cp = cp + e2_match_write_tables(buf, cp) + // HIT constant pool: lower the constant pool into rodata. // 池引用(mov r64, [rip+disp32] 占位)在函数发射期间记录(instr.cr 编码器); // 字符串区定稿后回填 disp(池区紧随字符串,8B/槽、8 对齐不破)。 diff --git a/src/format/elf/ld.cr b/src/format/elf/ld.cr index fb8cc27b..5b164feb 100644 --- a/src/format/elf/ld.cr +++ b/src/format/elf/ld.cr @@ -670,7 +670,7 @@ fn ctx_emit_dyn(buf: string, path: string) -> int: total := cell_load(g_ch_vaddr, (g_ch_count - 1) * 8) + cell_load(g_ch_size, (g_ch_count - 1) * 8) - g_text_base println(int_str(total)) emit(buf, total, 0) - fd := syscall3(2, path, 577, 493) // 0755: ELF output must be executable + fd := syscall3(2, path, 577, linux_mode_exec()) // 0755: executable ELF output if fd < 0: return -1 nw := syscall3(1, fd, buf, total) @@ -685,7 +685,7 @@ fn ctx_emit_so(buf: string, path: string) -> int: layout() total := cell_load(g_ch_vaddr, (g_ch_count - 1) * 8) + cell_load(g_ch_size, (g_ch_count - 1) * 8) - g_text_base emit(buf, total, 1) - fd := syscall3(2, path, 577, 493) // 0755: ELF output must be executable + fd := syscall3(2, path, 577, linux_mode_rw()) // 0644: shared object, not executable if fd < 0: return -1 nw := syscall3(1, fd, buf, total) @@ -898,7 +898,7 @@ fn ctx_emit_static(buf: string, path: string) -> int: bss_page := 0x400000 + ((total + 4095) / 4096) * 4096 + 4096 write_phdr(buf, 1, 1, 6, 0, bss_page, bss_page, 0, 1073741824, 4096) - fd := syscall3(2, path, 577, 493) // 0755: ELF output must be executable + fd := syscall3(2, path, 577, linux_mode_exec()) // 0755: executable ELF output if fd < 0: return -1 nw := syscall3(1, fd, buf, total) diff --git a/src/format/elf/resolve.cr b/src/format/elf/resolve.cr index fa65427d..53a4c4a9 100644 --- a/src/format/elf/resolve.cr +++ b/src/format/elf/resolve.cr @@ -51,7 +51,23 @@ fn res_labels(): g_label_count = 0 off : ., mut = 0 ii : ., mut = 0 - scratch := alloc(64) + // The encoder writes at pos=0 while measuring, and backward-label + // patches write into the same scratch. A sparse MATCH emits a fixed + // 30-byte block per value row, so size the scratch from the largest + // MATCH range in this function. The 512-byte floor covers every other + // single instruction. + scratch_sz : ., mut = 512 + ii = 0 + loop: + if ii >= ic: + break + if iri_op(ist + ii) == IR_MATCH: + cand := iri_s3(ist + ii) * 32 + 64 + if cand > scratch_sz: + scratch_sz = cand + ii = ii + 1 + scratch := alloc(scratch_sz) + ii = 0 loop: if ii >= ic: break diff --git a/src/kernel/bytes.cr b/src/kernel/bytes.cr index 8a1f11b9..c8c8cfe7 100644 --- a/src/kernel/bytes.cr +++ b/src/kernel/bytes.cr @@ -83,6 +83,14 @@ fn cell_store32(buf: string, pos: int, val: int): fn cell_load32(buf: string, pos: int) -> int: return r32(buf, pos) +// Signed 32-bit read. The wire uses signed i32 for STR indexes and NOD +// targets; cell_load32 returns the unsigned bit pattern. +fn cell_load_i32(buf: string, pos: int) -> int: + v := r32(buf, pos) + if v > 2147483647: + return v - 4294967296 + return v + fn cell_store8(buf: string, pos: int, val: int): w8(buf, pos, val) diff --git a/src/lattice/ent_kernel.cr b/src/lattice/ent_kernel.cr index eb789d85..11170f4d 100644 --- a/src/lattice/ent_kernel.cr +++ b/src/lattice/ent_kernel.cr @@ -112,6 +112,7 @@ fn compute_live_ranges(): if ii >= ic: break inst := ist + ii + op := nod_op(inst) d := nod_dest(inst) s1 := nod_s1(inst) s2 := nod_s2(inst) @@ -127,7 +128,7 @@ fn compute_live_ranges(): if cell_load(g_ir_live_ranges, st) < 0: cell_store(g_ir_live_ranges, st, ii) cell_store(g_ir_live_ranges, st + 8, ii) - if s2 >= vs && s2 < vs + vc: + if op != IR_MATCH && s2 >= vs && s2 < vs + vc: lv := s2 - vs st := (seg + lv) * 16 if cell_load(g_ir_live_ranges, st) < 0: @@ -922,7 +923,7 @@ fn rl_rule2_func(func_i: int, vs: int, vc: int, nst: int, nc: int, es: int, ec: s2 := nod_s2(inst) if s1 == gv && op != IR_STORE: rd = 1 - if s2 == gv: + if op != IR_MATCH && s2 == gv: rd = 1 if rd != 0 && inst >= ent_def(first_def): // 推进游标至最后一个 def ≤ inst 的版本 diff --git a/src/os/linux/file_mode.cr b/src/os/linux/file_mode.cr new file mode 100644 index 00000000..7b6da6f1 --- /dev/null +++ b/src/os/linux/file_mode.cr @@ -0,0 +1,8 @@ +// Linux output file modes. +// +// These are Linux OS facts, not target architecture or ELF header facts: +// 0755 executable ELF, 0644 non-executable data/shared object. +fn linux_mode_exec() -> int: + return 493 +fn linux_mode_rw() -> int: + return 420 diff --git a/src/targets/x86_64/linux/elf/_import.cr b/src/targets/x86_64/linux/elf/_import.cr index c30399b1..237372ef 100644 --- a/src/targets/x86_64/linux/elf/_import.cr +++ b/src/targets/x86_64/linux/elf/_import.cr @@ -42,6 +42,8 @@ import frame import resolve import elf import ld +// Linux output file modes: linux_mode_exec and linux_mode_rw. +import file_mode // Linux entry sequence: emit_start and emit_start_size. import entry // SysV AMD64 call sequences: cs_args_dispatch, cs_stack_args, cs_stack_cleanup, diff --git a/src/targets/x86_64/linux/elf/main.cr b/src/targets/x86_64/linux/elf/main.cr index 273a6183..981e001a 100644 --- a/src/targets/x86_64/linux/elf/main.cr +++ b/src/targets/x86_64/linux/elf/main.cr @@ -62,6 +62,8 @@ fn backend_has_extended_flags() -> int: return 1 if cli_has("dump-objects") != 0: return 1 + if cli_has("dump-match-plan") != 0: + return 1 if cli_has("dump-nod-types") != 0: return 1 if cli_has("dump-alloc-strides") != 0: @@ -151,7 +153,7 @@ fn corearch_main() -> int: g_elf_buf = alloc(16777216) sz := elf_gen(g_elf_buf) w16(g_elf_buf, 16, 3) - fd := syscall3(2, out_path, 577, 493) + fd := syscall3(2, out_path, 577, linux_mode_rw()) if fd < 0: print("error: cannot write ") println(out_path) @@ -222,7 +224,7 @@ fn corearch_main() -> int: ctx_set_user_code(cd, cs) sz = ctx_emit_static(g_elf_buf, out_path) else: - fd := syscall3(2, out_path, 577, 493) // 0755: ELF output must be executable + fd := syscall3(2, out_path, 577, linux_mode_exec()) // 0755: executable ELF output if fd < 0: print("error: cannot write ") println(out_path) @@ -244,7 +246,7 @@ fn corearch_main() -> int: return 1 else: sz := elf_gen(g_elf_buf) - fd := syscall3(2, out_path, 577, 493) // 0755: ELF output must be executable + fd := syscall3(2, out_path, 577, linux_mode_exec()) // 0755: executable ELF output if fd < 0: print("error: cannot write ") println(out_path) diff --git a/tests/selfhost/test_ccr_types.py b/tests/selfhost/test_ccr_types.py index de6fc9e0..24019794 100644 --- a/tests/selfhost/test_ccr_types.py +++ b/tests/selfhost/test_ccr_types.py @@ -1894,8 +1894,8 @@ def test_p4t4_dump_flag_zero_artifact_effect(): def test_p4t4_cir_snapshot_layout_unbumped(): - """㉛ `.cir` 快照**布局**未变(**P4 当时**未 bump 的证据面;**17 → 18 由 TODO #2026-09-16-16 批 2 换代**—— - 旧值 17 / 新值 18 / 归因 = 聚合读结果槽型改声明面形式(旧快照与新语义不等价)/ 出处 = 计划 §12.4·§13): + """㉛ `.cir` 快照**布局**未变(**P4 当时**未 bump 的证据面;**17 → 18 由 TODO #2026-09-16-16 批 2 换代**, + 后续 v23/v24 因语义/布局换代——v24 在指令段后新增 MATCH CASE 小节——旧值 17 / 新值 24 / 归因 = 聚合读结果槽型改声明面形式(旧快照与新语义不等价)/ 出处 = 计划 §12.4·§13): 条目版本位 == 当前版本; 节点小节 = 8B 计数 + n×64B(8 字段)——即「项槽不落盘、装载侧重派生」的格式 事实;>0 节点保证非空转。""" @@ -1910,7 +1910,7 @@ def test_p4t4_cir_snapshot_layout_unbumped(): total_nodes = 0 for f in entries: ver, nodes, ok = cir_entry_nodes(os.path.join(cache_dir, f)) - assert ver == 23, f"{f}: CIR_CACHE_VER {ver} != 23(2026-10-23 **BigInt IR 批** v23:新增 IR_CONST_BIG/IR_PTR_OF/IR_STR_OF;旧条目会复活旧整数语义 ⇒ 旧缓存必须失效;前代 22 = 2026-09-18 插值展开批、21 = 串域批、20 = 缓存膨胀批、19 = 批 5(opt-dex)、18 = TODO #2026-09-16-16 批 2)" + assert ver == 25, f"{f}: CIR_CACHE_VER {ver} != 25(2026-10-24 v10 MATCH CASE 语义模型持久化:CASE 小节存 target label 而非派生 wire target;前代 24 = MATCH CASE 持久化、23 = 2026-10-23 **BigInt IR 批**;22 = 插值展开批、21 = 串域批、20 = 缓存膨胀批、19 = 批 5(opt-dex)、18 = TODO #2026-09-16-16 批 2)" assert ok, f"{f}: node section (64B stride) runs past EOF" total_nodes += len(nodes) assert total_nodes > 0, "vacuous: no nodes parsed from snapshots" @@ -2073,7 +2073,7 @@ def test_p5t2_snapshot_disk_code_preserved(): disk = [] for f in sorted(os.listdir(cache_dir)): ver, nodes, ok = cir_entry_nodes(os.path.join(cache_dir, f)) - assert ver == 23 and ok, f"{f}: version/stride drifted(期望 23 = 2026-10-23 **BigInt IR 批**(IR_CONST_BIG/IR_PTR_OF/IR_STR_OF;前代 22 = 插值展开批;节点 stride 仍 64B / 头偏移不变(裁-4 + 尾部 trailer);前代 21 = 串域批、20 = 缓存膨胀批、19 = 批 5(opt-dex)、18 = TODO #2026-09-16-16 批 2)" + assert ver == 25 and ok, f"{f}: version/stride drifted(期望 25 = 2026-10-24 v10 MATCH CASE 语义模型持久化(CASE 小节存 target label,节点 stride 仍 64B / 头偏移不变);前代 24 = MATCH CASE 持久化;23 = BigInt IR 批;22 = 插值展开批;21 = 串域批;20 = 缓存膨胀批;19 = 批 5(opt-dex);18 = TODO #2026-09-16-16 批 2)" disk += [(nd[0], nd[5]) for nd in nodes] assert disk, "vacuous: no nodes parsed from snapshots" mint = set((r[1], r[2]) for r in rows if r[1] in MINT_OPS and r[2] > 0) diff --git a/tests/selfhost/test_directory_build.py b/tests/selfhost/test_directory_build.py index 2e8d2ee5..f60b81c7 100644 --- a/tests/selfhost/test_directory_build.py +++ b/tests/selfhost/test_directory_build.py @@ -61,8 +61,15 @@ def main() -> int: print(result.stderr) return 1 + exec_bits = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH output_mode = output.stat().st_mode - output.chmod(output_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + if not output_mode & exec_bits: + print( + f"[FAIL] directory output has no execute bit: mode {output_mode:o}" + ) + print(result.stdout) + print(result.stderr) + return 1 run_result = subprocess.run([str(output)], cwd=project, timeout=30) if run_result.returncode != 23: print(f"[FAIL] directory output returned {run_result.returncode}, expected 23") @@ -73,6 +80,48 @@ def main() -> int: "[PASS] `corec build .` resolves _import.cr and emits a working ELF" ) + # Shared outputs (.so) are data, not executables: Linux mode 0644. + shared_ccr = project / "shared.ccr" + shared_compile = run_corec( + ["ccr", ".", "--static", "-o", str(shared_ccr), "--opt-level", "0"], + project, + ) + if shared_compile.returncode != 0 or not shared_ccr.exists(): + print(f"[FAIL] shared ccr build exited {shared_compile.returncode}") + print(shared_compile.stdout) + print(shared_compile.stderr) + return 1 + shared_output = project / "libdirectory_build_smoke.so" + shared_result = subprocess.run( + [ + "nice", + "-n", + "19", + str(COREARCH), + str(shared_ccr), + "--elf", + "--shared", + "-o", + str(shared_output), + ], + cwd=project, + capture_output=True, + text=True, + timeout=180, + ) + if shared_result.returncode != 0 or not shared_output.exists(): + print(f"[FAIL] --shared build exited {shared_result.returncode}") + print(shared_result.stdout) + print(shared_result.stderr) + return 1 + shared_mode = shared_output.stat().st_mode & 0o777 + if shared_mode != 0o644: + print( + f"[FAIL] --shared output mode is {shared_mode:o}, expected 644" + ) + return 1 + print("[PASS] --shared output is mode 0644, not executable") + (project / "helper.cr").write_text( "fn imported_result() -> int:\n return 25\n", encoding="utf-8" ) @@ -103,9 +152,14 @@ def main() -> int: print(unnamed_result.stdout) print(unnamed_result.stderr) return 1 - unnamed_output.chmod( - unnamed_output.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH - ) + if not unnamed_output.stat().st_mode & exec_bits: + print( + "[FAIL] unnamed directory output has no execute bit: mode " + f"{unnamed_output.stat().st_mode:o}" + ) + print(unnamed_result.stdout) + print(unnamed_result.stderr) + return 1 unnamed_run = subprocess.run([str(unnamed_output)], cwd=unnamed, timeout=30) if unnamed_run.returncode != 24: print( diff --git a/tests/selfhost/test_interp_parity.py b/tests/selfhost/test_interp_parity.py index 34285862..c8f64426 100644 --- a/tests/selfhost/test_interp_parity.py +++ b/tests/selfhost/test_interp_parity.py @@ -252,7 +252,7 @@ EXPECT_OPS = { "enum_payload_in_callee": {"make_enum", "load_enum_tag"}, "enum_construct_callee_match_main": {"call", "load_enum_tag"}, - "enum_tag_variant_in_callee": {"make_enum", "load_enum_tag"}, + "enum_tag_variant_in_callee": {"make_enum", "match"}, "enum_payload_tag_only_in_callee": {"make_enum", "load_enum_tag"}, "bare_ptr_deref_callee": {"addr_index", "deref"}, "ptr_store_callee": {"ref", "store_ptr"}, diff --git a/tests/selfhost/test_match_exhaust.py b/tests/selfhost/test_match_exhaust.py index 73c303f9..95a98679 100644 --- a/tests/selfhost/test_match_exhaust.py +++ b/tests/selfhost/test_match_exhaust.py @@ -7,7 +7,8 @@ / payload 变体 / 泛型枚举实例 / 单变体枚举 / 枚举形参位 负:缺臂 ⇒ rc=1 + `error[TM03]` + 反例含具体缺失**变体名** + **无产物**(绝不静默 rc=0—— 修复前实测:缺臂产物照出,未匹配值静默得 0);无臂 ⇒ 同上;`check` 子命令同拒 - 登记:非枚举域不判穷尽(int 匹配 rc=0);不可映射模式(枚举匹配里混字面量臂)不判穷尽 rc=0 + 字面量域:int/string/char 无 `_`/绑定臂 ⇒ rc=1 + `error[TM03]` + 无产物(Task 5a); + 枚举匹配里混字面量臂同样按字面量域判(不再静默放行) 软面:重复臂 ⇒ `error[TM04]` **非硬门**(rc=0 + 产物照出)——诊断面登记,非 rc 面收紧 背景(Task 3 前实测,本套件即其回归):`EC_TM_*`(9001-9009)此前仅定义零 raise,match @@ -195,12 +196,14 @@ def main(): case_run_reject("run_path_rejected", COLOR + "fn main() -> int:\n c := Blue()\n return match c:\n Red => 1, Green => 2,\n", ["error[TM03]", "missing variant 'Blue'"]), - # 登记:非枚举域不判穷尽(int 字面量匹配 ⇒ 零诊断,rc=0) - case_run("non_enum_domain_not_judged", - "fn main() -> int:\n n := 3\n return match n:\n 1 => 10, 2 => 20,\n", 0), - # 登记:不可映射模式(枚举匹配混字面量臂)不判穷尽(-1 = 不判,不得当「不穷尽」) - case_run("unmappable_pattern_not_judged", - "enum Color2:\n Red2, Green2, Blue2\nfn main() -> int:\n c := Blue2()\n return match c:\n Red2 => 1, 5 => 2,\n", 0), + # 字面量域(Task 5a):int 无通配/绑定臂 ⇒ rc=1 + TM03 + 无产物 + case_reject("literal_domain_requires_wildcard", + "fn main() -> int:\n n := 3\n return match n:\n 1 => 10, 2 => 20,\n", + ["error[TM03]", "Non-exhaustive match", "add a wildcard or binding arm"]), + # 枚举混字面量臂:字面量仍是 refutable,无通配/绑定臂 ⇒ 同拒 + case_reject("mixed_literal_enum_requires_wildcard", + "enum Color2:\n Red2, Green2, Blue2\nfn main() -> int:\n c := Blue2()\n return match c:\n Red2 => 1, 5 => 2,\n", + ["error[TM03]", "Non-exhaustive match"]), # 软面:重复臂 ⇒ TM04 非硬门(rc=0 + 产物照出) case_soft("redundant_arm_soft", COLOR + "fn main() -> int:\n c := Red()\n return match c:\n Red => 1, Red => 2, Green => 3, Blue => 4,\n", diff --git a/tests/selfhost/test_multibranch_v10.py b/tests/selfhost/test_multibranch_v10.py new file mode 100644 index 00000000..c7693eb7 --- /dev/null +++ b/tests/selfhost/test_multibranch_v10.py @@ -0,0 +1,3746 @@ +#!/usr/bin/env python3 +"""v10 multibranch wire tests: IR_MATCH opcode and the CASE segment (tag 9). + +CASE is the last v10 segment. Its body is: + + case_count u32 + case_count x 24B { + value i64 @0 // signed scrutinee value (kind VALUE/ENUM) + kind u32 @8 // 0=value 1=enum 2=string 3=default + str_index i32 @12 // STR index, or -1 when unused + target_nod i32 @16 // NOD id of an IR_LABEL node + flags u32 @20 // reserved; zero in v10 + } + +The tests below check the physical layout with an independent parser, then +prove the real loader accepts IR_MATCH (54) and nonempty CASE bodies and rejects +out-of-domain case records. Synthetic CASE bodies are installed into a real +`corec` image so the rest of the container stays authoritative. + +Runs standalone (`python3 tests/selfhost/test_multibranch_v10.py`) and is also +pytest-discoverable (stdlib only). +""" + +import struct +import subprocess +import sys +import tempfile +import time +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from ccr_v10_model import ( # noqa: E402 + BODY_START, + SEG_TAGS, + encode, + flags, + read_segment_table, + segment, + validate, +) + +BASE = Path(__file__).resolve().parent.parent.parent +COREC = BASE / "build" / "corec" +COREARCH = BASE / "build" / "corearch" + +IR_LABEL = 21 +IR_MATCH = 54 + +CCR_CASE_VALUE = 0 +CCR_CASE_KIND = 8 +CCR_CASE_STR = 12 +CCR_CASE_TARGET = 16 +CCR_CASE_FLAGS = 20 +CCR_CASE_DISK = 24 + +KIND_VALUE = 0 +KIND_ENUM = 1 +KIND_STRING = 2 +KIND_DEFAULT = 3 + +CASE_TAG = 9 +NOD_TAG = 5 + +# Two independent ifs give several LABEL nodes and a nonempty STR table, so a +# synthetic CASE can point at real graph labels and a real string. +BRANCH_SOURCE = """fn main() -> int: + x := 1 + if x == 2: + return 9 + if x == 3: + return 8 + return 7 +""" + +EXPECTED_EXIT = 7 + + +class CaseParseError(ValueError): + """Raised when a CASE body is not the exact 24B table v10 defines.""" + + +def _run(cmd, timeout=180, cwd=None): + return subprocess.run( + [str(part) for part in cmd], + capture_output=True, + text=True, + cwd=str(cwd if cwd is not None else BASE), + timeout=timeout, + ) + + +def _build_ccr(tmp, source=BRANCH_SOURCE, name="multibranch", cwd=None): + src = tmp / f"{name}.cr" + ccr = tmp / f"{name}.ccr" + src.write_text(source, encoding="utf-8") + r = _run([COREC, "ccr", src, "-o", ccr], cwd=cwd) + assert r.returncode == 0, f"corec ccr failed: {r.stdout}\n{r.stderr}" + data = ccr.read_bytes() + validate(data) + return data + + +def _split(data): + bodies = {} + for tag, offset, size in read_segment_table(data): + bodies[tag] = data[offset : offset + size] + return bodies + + +def _replace_case(data, body): + bodies = _split(data) + bodies[CASE_TAG] = body + out = encode(bodies, flags=flags(data)) + validate(out) + return out + + +def _pack_case(records): + """Pack `(value, kind, str_index, target_nod, flags)` records.""" + body = struct.pack("/.core/cache/cir and is keyed by source path and +# function name. These helpers parse the v25 per-function snapshot independently +# so the tests can prove the semantic MATCH CASE model round-trips, and can +# tamper with exactly that section. + +CIR_VERSION = 25 +CIR_CASE_ROW = 24 + + +def _cache_dir(tmp): + return tmp / ".core" / "cache" / "cir" + + +def _clean_cache(tmp): + import shutil + + shutil.rmtree(_cache_dir(tmp), ignore_errors=True) + + +def _cache_entry(tmp, fn): + entries = [ + p + for p in sorted(_cache_dir(tmp).glob("*.cir")) + if p.name.endswith(f"::{fn}.cir") + ] + assert len(entries) == 1, f"{fn}: expected one cache entry, got {entries}" + return entries[0] + + +def _entry_stamp(path): + st = path.stat() + return (st.st_size, st.st_mtime_ns) + + +def _cir_snapshot(data): + """Parse a v25 snapshot. Returns section offsets and CASE model rows.""" + if len(data) < 56: + raise AssertionError(f"snapshot too short: {len(data)}B") + magic, ver, cid, fp, sig, name_len = struct.unpack_from("= 3, f"probe source has only {len(labels)} labels" + str_count = struct.unpack_from(" 0, "probe source has no strings" + + records = [ + (7, KIND_VALUE, -1, labels[0], 0), + (2, KIND_ENUM, -1, labels[1], 0), + (0, KIND_STRING, 0, labels[2], 0), + (0, KIND_DEFAULT, -1, labels[0], 0), + ] + body = _pack_case(records) + out = _replace_case(data, body) + + assert segment(out, CASE_TAG) == body + table = read_segment_table(out) + _tag, case_off, case_size = table[CASE_TAG - 1] + assert case_size == 4 + len(records) * CCR_CASE_DISK == 4 + 4 * 24 + assert case_off + case_size == len(out), "CASE must be the final segment" + + rows = _parse_case(segment(out, CASE_TAG)) + assert [row["value"] for row in rows] == [7, 2, 0, 0] + assert [row["kind"] for row in rows] == list(range(4)) + assert [row["str_index"] for row in rows] == [-1, -1, 0, -1] + assert [row["target_nod"] for row in rows] == [labels[0], labels[1], labels[2], labels[0]] + assert [row["flags"] for row in rows] == [0, 0, 0, 0] + # 24B stride and field offsets are the wire contract, not a comment. + assert [row["offset"] for row in rows] == [4, 28, 52, 76] + assert rows[0]["offset"] + CCR_CASE_VALUE == 4 + assert rows[0]["offset"] + CCR_CASE_KIND == 12 + assert rows[0]["offset"] + CCR_CASE_STR == 16 + assert rows[0]["offset"] + CCR_CASE_TARGET == 20 + assert rows[0]["offset"] + CCR_CASE_FLAGS == 24 + + # Deterministic: re-encoding the split image, twice, reproduces the bytes. + assert _pack_case(records) == body + assert encode(_split(out), flags=flags(out)) == out + assert encode(_split(out), flags=flags(out)) == encode(_split(out), flags=flags(out)) + + +def test_case_segment_is_last_after_edg(): + with tempfile.TemporaryDirectory() as td: + data = _build_ccr(Path(td)) + table = read_segment_table(data) + assert [tag for tag, _o, _s in table] == list(SEG_TAGS) + assert table[0][1] == BODY_START + assert table[CASE_TAG - 1][1] + table[CASE_TAG - 1][2] == len(data) + + +# --------------------------------------------------------------------------- +# 2. IR_MATCH is a known v10 opcode +# --------------------------------------------------------------------------- + + +def test_ir_match_opcode_is_known_to_loader(): + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + data = _build_ccr(tmp) + labels = _label_nodes(data) + assert len(labels) >= 3 + nod_off = None + for tag, offset, _size in read_segment_table(data): + if tag == NOD_TAG: + nod_off = offset + break + assert nod_off is not None + + # A MATCH node plus three CASE records, as the schema expects them to + # coexist. s2/s3 carry the CASE range: s2=0, s3=count. Removing + # IR_MATCH from ccr_nod_src3_semantic would reject s3=3 as a legacy + # target fact, so this fixture pins the semantic field. The range is + # homogeneous (all VALUE + DEFAULT); mixed ENUM/VALUE is rejected by the + # loader rule and is covered separately. + records = [ + (7, KIND_VALUE, -1, labels[0], 0), + (2, KIND_VALUE, -1, labels[1], 0), + (0, KIND_DEFAULT, -1, labels[2], 0), + ] + with_match = _replace_case(data, _pack_case(records)) + d = bytearray(with_match) + struct.pack_into("= 3 + + records = [ + (7, KIND_VALUE, -1, labels[0], 0), + (2, KIND_ENUM, -1, labels[1], 0), + (0, KIND_STRING, 0, labels[2], 0), + (0, KIND_DEFAULT, -1, labels[0], 0), + ] + out = _replace_case(data, _pack_case(records)) + + # Container and loader: CASE is consumed, not rejected as an unknown + # shape. + _assert_loads(out, tmp, "nonempty_case") + + ccr = tmp / "nonempty_case.elf.ccr" + ccr.write_bytes(out) + elf = tmp / "nonempty_case.elf" + r = _run([COREARCH, ccr, "--elf", "--static", "-o", elf]) + assert r.returncode == 0, r.stdout + r.stderr + assert elf.exists() + run = subprocess.run([str(elf)], capture_output=True, text=True, timeout=10) + assert run.returncode == EXPECTED_EXIT, ( + f"case probe returned {run.returncode}, expected {EXPECTED_EXIT}" + ) + + +def test_empty_case_stays_empty_and_loads(): + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + data = _build_ccr(tmp) + assert segment(data, CASE_TAG) == struct.pack(" int: + return match n: + 10 => 1, + 20 => 2, + 30 => 3, + 40 => 4, + _ => 5 + +fn main() -> int: + if pick(10) != 1: + return 10 + if pick(20) != 2: + return 20 + if pick(30) != 3: + return 30 + if pick(40) != 4: + return 40 + if pick(99) != 5: + return 99 + return 42 +""" + +MATCH_BOOL_SOURCE = """fn pickb(b: bool) -> int: + return match b: + true => 7, + false => 8, + _ => 9 + +fn main() -> int: + if pickb(true) != 7: + return 1 + if pickb(false) != 8: + return 2 + return 42 +""" + +MATCH_ENUM_SOURCE = """enum Color: + Red, Green, Blue + +fn pickc(c: Color) -> int: + return match c: + Red => 11, + Green => 22, + Blue => 33, + _ => 44 + +fn main() -> int: + if pickc(Red()) != 11: + return 1 + if pickc(Green()) != 22: + return 2 + if pickc(Blue()) != 33: + return 3 + return 42 +""" + +# Every function here has at least one arm outside the supported set, so each +# must keep the old sequential lowering. The programs are not executed: the old +# path's string/struct semantics are Task 4/5 work, and this fixture only pins +# that corec does not encode them as MATCH. +MATCH_UNSUPPORTED_SOURCE = """struct P: + x: int + +fn pickstr(s: string) -> int: + return match s: + "a" => 1, + _ => 2 + +fn pickid(n: int) -> int: + return match n: + x => x + 1 + +fn pickp(p: P) -> int: + return match p: + P { x = _ } => 1, + _ => 2 + +fn main() -> int: + return 0 +""" + +MATCH_TWO_FUNCS_SOURCE = """fn pick_a(n: int) -> int: + return match n: + 1 => 10, + 2 => 20, + _ => 30 + +fn pick_b(n: int) -> int: + return match n: + 7 => 70, + 8 => 80, + 9 => 90, + _ => 99 + +fn main() -> int: + if pick_a(2) != 20: + return 1 + if pick_b(9) != 90: + return 2 + if pick_b(100) != 99: + return 3 + return 42 +""" + + +MATCH_MIXED_SOURCE = """enum Color: + Red, Green, Blue + +fn pick(c: Color) -> int: + return match c: + Red => 1, + 2 => 2, + _ => 3 + +fn main() -> int: + if pick(Red()) != 1: + return 1 + if pick(Green()) != 3: + return 2 + return 42 +""" + +# The checker requires a wildcard/binding arm for literal matches. These two +# sources keep one so they compile; the no-DEFAULT backend shape is produced by +# _drop_match_default on the resulting .ccr image. +MATCH_NO_DEFAULT_SOURCE = """fn pick(n: int) -> int: + return match n: + 1 => 11, + 2 => 22, + _ => 5 + +fn main() -> int: + return pick(99) +""" + +MATCH_NO_DEFAULT_HIT_SOURCE = """fn pick(n: int) -> int: + return match n: + 1 => 11, + 2 => 22, + _ => 0 + +fn main() -> int: + return pick(1) + pick(2) +""" + +MATCH_LOOP_SOURCE = """fn f() -> int: + x : int + sum : int = 0 + i : int = 0 + loop: + if i >= 5: + break + match i: + 0 =>: + x = 10 + 1 =>: + sum = sum + x + _ =>: + sum = sum + x + i = i + 1 + return sum + 2 + +fn main() -> int: + return f() +""" + +MATCH_IDENT_FALLBACK_SOURCE = """fn pickid(n: int) -> int: + return match n: + x => 42 + +fn main() -> int: + return pickid(7) +""" + +# Task 5a: string, char, and ident patterns use the sequential path. None of +# them may become a CASE row (strings are not integers; char/binding rows would +# change evaluation order). +MATCH_STRING_SOURCE = """fn pick(s: string) -> int: + return match s: + "a" => 1, + "b" => 2, + _ => 3 + +fn main() -> int: + if pick("a") != 1: + return 10 + if pick("b") != 2: + return 11 + if pick("zzz") != 3: + return 12 + return 42 +""" + +MATCH_CHAR_SOURCE = """fn code_a() -> int: + return 'A' as int + +fn pick(c: char) -> int: + return match c: + 'A' => 1, + 'B' => 2, + _ => 3 + +fn main() -> int: + a := code_a() + if a == 0: + return 9 + c := a as char + if pick(c) != 1: + return 10 + if pick('B') != 2: + return 11 + if pick('C') != 3: + return 12 + return 42 +""" + +MATCH_IDENT_BIND_SOURCE = """fn inc(n: int) -> int: + return match n: + x => x + 1 + +fn pick(s: string) -> string: + return match s: + x => x + +fn main() -> int: + if inc(7) != 8: + return 1 + if pick("hi") != "hi": + return 2 + v := match 5: + 1 => 10, + rest => rest + if v != 5: + return 3 + return 42 +""" + +MATCH_STRING_NO_DEFAULT_SOURCE = """fn pick(s: string) -> int: + return match s: + "a" => 1, + "b" => 2 + +fn main() -> int: + return pick("a") +""" + +MATCH_CHAR_NO_DEFAULT_SOURCE = """fn pick(c: char) -> int: + return match c: + 'A' => 1, + 'B' => 2 + +fn main() -> int: + return pick('A') +""" + +MATCH_INT_NO_DEFAULT_SOURCE = """fn pick(n: int) -> int: + return match n: + 1 => 1, + 2 => 2 + +fn main() -> int: + return pick(1) +""" + +MATCH_BOOL_NO_DEFAULT_SOURCE = """fn pick(b: bool) -> int: + return match b: + true => 1, + false => 2 + +fn main() -> int: + return pick(true) +""" + +MATCH_WILDCARD_ORDER_SOURCE = """fn pick(n: int) -> int: + return match n: + _ => 1, + 2 => 2 + +fn main() -> int: + return pick(2) +""" + +MATCH_IDENT_ORDER_SOURCE = """fn pick(n: int) -> int: + return match n: + x => 1, + 2 => 2 + +fn main() -> int: + return pick(2) +""" + +MATCH_DUPLICATE_STRING_CHAR_SOURCE = """fn picks(s: string) -> int: + return match s: + "a" => 1, + "a" => 2, + _ => 3 + +fn pickc(c: char) -> int: + return match c: + 'A' => 1, + 'A' => 2, + _ => 3 + +fn main() -> int: + return picks("a") + pickc('A') +""" + +MATCH_INTERP_PATTERN_SOURCE = """fn pick(x: int) -> int: + s := "a" + return match s: + "a${x}" => 1, + _ => 2 + +fn main() -> int: + return pick(1) +""" + +MATCH_NESTED_LITERAL_SOURCE = """enum E: + A(int), B + +fn main() -> int: + e := A(1) + return match e: + A(1) => 1, + _ => 2 +""" + +# Composite sub-patterns are allocated before the outer EXPR_ENUMPAT node, so +# they are not contiguous from ast_b. These fixtures pin both orders and a +# deeper nesting; all must be rejected rather than matched as a bare variant. +MATCH_NESTED_SECOND_BYPASS_SOURCE = """enum Inner: + X(int), Y(int) + +enum Outer: + A(Inner, Inner), B + +fn main() -> int: + o := A(X(1), X(2)) + return match o: + A(x, X(y)) => 1, + _ => 2 +""" + +MATCH_NESTED_FIRST_BYPASS_SOURCE = """enum Inner: + X(int), Y(int) + +enum Outer: + A(Inner, Inner), B + +fn main() -> int: + o := A(X(1), X(2)) + return match o: + A(X(y), x) => 1, + _ => 2 +""" + +MATCH_NESTED_DEEP_BYPASS_SOURCE = """enum L3: + Z(int) + +enum L2: + Y(L3) + +enum L1: + X(L2) + +enum Outer: + A(L1), B + +fn f(o: Outer) -> int: + return match o: + A(X(Y(z))) => 1, + _ => 2 + +fn main() -> int: + return f(B()) +""" + +MATCH_MULTI_PAYLOAD_BIND_SOURCE = """enum Pair: + A(int, int), B + +fn sum(p: Pair) -> int: + return match p: + A(x, y) => x + y, + B => 0 + +fn second(p: Pair) -> int: + return match p: + A(_, x) => x, + B => 0 + +fn main() -> int: + if sum(A(3, 4)) != 7: + return 1 + if sum(B()) != 0: + return 2 + if second(A(9, 8)) != 8: + return 3 + if second(B()) != 0: + return 4 + return 42 +""" + +# Task 5b1: struct patterns. Field names are bound by name (the pattern below +# reverses the declaration order in the second arm), ident subpatterns bind the +# field value, and a miss must fall to the wildcard instead of the first arm. +MATCH_STRUCT_SOURCE = """struct Point: + x: int + y: int + +fn pick(p: Point) -> int: + return match p: + Point { x = 1, y = yv } => yv, + Point { y = 9, x = xv } => xv, + _ => 0 + +fn main() -> int: + if pick(Point { x = 1, y = 7 }) != 7: + return 1 + if pick(Point { x = 4, y = 9 }) != 4: + return 2 + if pick(Point { x = 2, y = 3 }) != 0: + return 3 + return 42 +""" + +# Scalar field kinds: bool/char/string/int literals all compare through the +# sequential path (strings via str_eq, not a CASE row). +MATCH_STRUCT_SCALARS_SOURCE = """struct Rec: + flag: bool + ch: char + name: string + n: int + +fn pick(r: Rec) -> int: + return match r: + Rec { n = 3, name = "hi", ch = 'A', flag = true } => 1, + Rec { flag = false, ch = 'B', name = "yo", n = 4 } => 2, + _ => 3 + +fn main() -> int: + if pick(Rec { flag = true, ch = 'A', name = "hi", n = 3 }) != 1: + return 1 + if pick(Rec { flag = false, ch = 'B', name = "yo", n = 4 }) != 2: + return 2 + if pick(Rec { flag = true, ch = 'B', name = "hi", n = 3 }) != 3: + return 3 + if pick(Rec { flag = true, ch = 'A', name = "no", n = 3 }) != 3: + return 4 + return 42 +""" + +# An irrefutable binding arm may close a struct match. +MATCH_STRUCT_IDENT_LAST_SOURCE = """struct Point: + x: int + y: int + +fn pick(p: Point) -> int: + return match p: + Point { x = 1, y = 2 } => 1, + rest => 2 + +fn main() -> int: + if pick(Point { x = 1, y = 2 }) != 1: + return 1 + if pick(Point { x = 0, y = 0 }) != 2: + return 2 + return 42 +""" + +MATCH_STRUCT_UNKNOWN_TYPE_SOURCE = """struct Point: + x: int + y: int + +fn main() -> int: + p := Point { x = 1, y = 2 } + return match p: + Nope { x = 1 } => 1, + _ => 0 +""" + +MATCH_STRUCT_UNKNOWN_FIELD_SOURCE = """struct Point: + x: int + y: int + +fn main() -> int: + p := Point { x = 1, y = 2 } + return match p: + Point { z = 1 } => 1, + _ => 0 +""" + +MATCH_STRUCT_WRONG_FIELD_TYPE_SOURCE = """struct Point: + x: int + y: int + +fn main() -> int: + p := Point { x = 1, y = 2 } + return match p: + Point { x = true } => 1, + _ => 0 +""" + +MATCH_STRUCT_DUP_FIELD_SOURCE = """struct Point: + x: int + y: int + +fn main() -> int: + p := Point { x = 1, y = 2 } + return match p: + Point { x = 1, x = 2 } => 1, + _ => 0 +""" + +MATCH_STRUCT_NESTED_SOURCE = """struct Point: + x: int + y: int + +struct Outer: + p: Point + +fn main() -> int: + o := Outer { p = Point { x = 1, y = 2 } } + return match o: + Outer { p = Point { x = 1 } } => 1, + _ => 0 +""" + +MATCH_STRUCT_NO_WILDCARD_SOURCE = """struct Point: + x: int + y: int + +fn main() -> int: + p := Point { x = 1, y = 2 } + return match p: + Point { x = 1 } => 1 +""" + +MATCH_STRUCT_WILDCARD_ORDER_SOURCE = """struct Point: + x: int + y: int + +fn main() -> int: + p := Point { x = 1, y = 2 } + return match p: + _ => 0, + Point { x = 1 } => 1 +""" + +# Runtime-constructed string field: the pattern must compare contents via str_eq, +# not intern indices/pointers. Also exercises partial, reordered fields and a +# non-int ident binding (string). +MATCH_STRUCT_MISC_SOURCE = """struct Point: + x: int + y: int + +struct R: + name: string + +fn pickp(p: Point) -> int: + return match p: + Point { y = 9, x = 5 } => 1, + Point { x = 2 } => 2, + _ => 0 + +fn pickname(r: R) -> string: + return match r: + R { name = n } => n, + _ => "" + +fn pickmatch(r: R) -> int: + return match r: + R { name = "hi" } => 1, + _ => 0 + +fn main() -> int: + if pickp(Point { x = 5, y = 9 }) != 1: + return 1 + if pickp(Point { x = 2, y = 3 }) != 2: + return 2 + if pickp(Point { x = 3, y = 3 }) != 0: + return 3 + r := R { name = concat("h", "i") } + if pickname(r) != "hi": + return 4 + if pickmatch(r) != 1: + return 5 + return 42 +""" + +MATCH_STRUCT_GENERIC_SCRUTINEE_SOURCE = """struct Point: + x: int + y: int + +struct One: + a: int + +fn pick[T](v: T) -> int: + return match v: + Point { x = 1 } => 1, + _ => 0 + +fn main() -> int: + return pick(One { a = 2 }) + pick(7) +""" + +MATCH_STRUCT_GENERIC_FIELD_SOURCE = """struct Pair[T]: + a: T + n: int + +fn pick(p: Pair[string]) -> int: + return match p: + Pair { a = 1, n = 2 } => 1, + _ => 0 + +fn main() -> int: + p := Pair { a = "x", n = 2 } + return pick(p) +""" + +# A concrete generic application inside a generic function still monomorphizes: +# the scrutinee is Pair[Point], so field substitution yields Point and the +# binding pattern is valid. This guards the monomorph revalidation from being +# over-conservative. +MATCH_STRUCT_GENERIC_INSTANCE_SOURCE = """struct Point: + x: int + y: int + +struct Pair[T]: + a: T + n: int + +fn show[U](u: U, p: Pair[Point]) -> int: + return match p: + Pair { a = v, n = 2 } => 1, + _ => 0 + +fn main() -> int: + p := Pair { a = Point { x = 1, y = 2 }, n = 2 } + if show(7, p) != 1: + return 1 + return 42 +""" + +# Concrete generic instances reached through locals, field access, call results, +# and a shadowed parameter. The checker resolves the actual scrutinee type; the +# monomorph defensiveness must not fall back to the generic struct declaration. +MATCH_STRUCT_GENERIC_ACTUALS_SOURCE = """struct Point: + x: int + +struct Pair[T]: + a: T + +struct Holder: + p: Pair[Point] + +fn make() -> Pair[Point]: + return Pair { a = Point { x = 7 } } + +fn via_local[U](u: U) -> int: + p := Pair { a = Point { x = 7 } } + return match p: + Pair { a = v } => v.x, + _ => 0 + +fn via_field[U](u: U, h: Holder) -> int: + return match h.p: + Pair { a = v } => v.x, + _ => 0 + +fn via_call[U](u: U) -> int: + return match make(): + Pair { a = v } => v.x, + _ => 0 + +fn shadowed[U](p: U) -> int: + p := Pair { a = Point { x = 7 } } + return match p: + Pair { a = v } => v.x, + _ => 0 + +fn main() -> int: + h := Holder { p = Pair { a = Point { x = 7 } } } + if via_local(1) != 7: + return 1 + if via_field(1, h) != 7: + return 2 + if via_call(1) != 7: + return 3 + if shadowed(99) != 7: + return 4 + return 42 +""" + +# Optional generic field: R[string] makes s: T? bind as string?. A literal cannot +# match an optional slot. The nested optional match is a valid operation on the +# bound value and runs through the interpreter (the pre-existing ELF layout for +# optional string struct fields is outside this pattern task). +MATCH_STRUCT_OPTIONAL_BIND_SOURCE = """struct R[T]: + s: T? + +fn show(r: R[string]) -> int: + return match r: + R { s = v } =>: + return match v: + Some(x) =>: + if x == "hi": + return 42 + return 1 + none =>: + return 2 + _ => 3 + +fn main() -> int: + r := R { s = Some("hi") } + return show(r) +""" + +MATCH_STRUCT_OPTIONAL_LITERAL_SOURCE = """struct R[T]: + s: T? + +fn pick(r: R[string]) -> int: + return match r: + R { s = "hi" } => 1, + _ => 0 + +fn main() -> int: + r := R { s = Some("hi") } + return pick(r) +""" + +MATCH_STRUCT_OPTIONAL_UNRESOLVED_SOURCE = """struct R[T]: + s: T? + +fn pick[U](r: R[U]) -> int: + return match r: + R { s = v } => 1, + _ => 0 + +fn main() -> int: + return pick(1) +""" + +MATCH_STRUCT_CONCRETE_MISMATCH_SOURCE = """struct Point: + x: int + y: int + +struct Other: + a: int + +fn main() -> int: + o := Other { a = 1 } + return match o: + Point { x = 1 } => 1, + _ => 0 +""" + +# Task 5b2: tuple patterns. Positional wrappers bind elements and test scalar +# literals; `_` skips an element. The literal string is built at runtime inside +# main so the comparison must use str_eq (content), not intern indices. +MATCH_TUPLE_SOURCE = """fn bind(n: int, s: string) -> int: + t := (n, s) + return match t: + (x, y) =>: + if x == 1: + if y == "hi": + return 1 + return 2 + _ => 3 + +fn lit(n: int, s: string) -> int: + t := (n, s) + return match t: + (1, "hi") => 1, + (2, "yo") => 2, + _ => 0 + +fn part(n: int, s: string) -> int: + t := (n, s) + return match t: + (a, _) => a, + _ => 9 + +fn mixed(flag: bool, ch: char, n: int) -> int: + t := (flag, ch, n) + return match t: + (true, 'A', 7) => 1, + (false, 'B', 8) => 2, + _ => 0 + +fn ident_last(n: int) -> int: + t := (n, n + 1) + return match t: + (1, 2) => 1, + rest =>: + if (rest . 0) == n: + return 2 + return 3 + +fn main() -> int: + if bind(1, "hi") != 1: + return 1 + if bind(2, "hi") != 2: + return 2 + if lit(1, concat("h", "i")) != 1: + return 3 + if lit(2, "yo") != 2: + return 4 + if lit(1, "yo") != 0: + return 5 + if part(7, "x") != 7: + return 6 + if mixed(true, 'A', 7) != 1: + return 7 + if mixed(false, 'B', 8) != 2: + return 8 + if mixed(false, 'A', 7) != 0: + return 9 + if ident_last(5) != 2: + return 10 + if ident_last(1) != 1: + return 11 + return 42 +""" + +MATCH_TUPLE_ARITY_SOURCE = """fn main() -> int: + t := (1, 2) + return match t: + (a, b, c) => 1, + _ => 0 +""" + +MATCH_TUPLE_NONTUPLE_SOURCE = """fn main() -> int: + n := 1 + return match n: + (a, b) => 1, + _ => 0 +""" + +MATCH_TUPLE_WRONG_TYPE_SOURCE = """fn main() -> int: + t := (1, 2) + return match t: + (a, "s") => 1, + _ => 0 +""" + +MATCH_TUPLE_NESTED_SOURCE = """fn main() -> int: + t := ((1, 2), 3) + return match t: + ((a, b), c) => 1, + _ => 0 +""" + +MATCH_TUPLE_NO_WILDCARD_SOURCE = """fn main() -> int: + t := (1, 2) + return match t: + (1, 2) => 1 +""" + +MATCH_TUPLE_WILDCARD_ORDER_SOURCE = """fn main() -> int: + t := (1, 2) + return match t: + _ => 0, + (1, 2) => 1 +""" + +MATCH_TUPLE_GROUPING_SOURCE = """fn main() -> int: + n := 7 + r := match n: + (a) => a + if r != 7: + return 1 + return 42 +""" + +MATCH_TUPLE_BAD_LEADING_COMMA_SOURCE = """fn main() -> int: + n := 1 + return match n: + (,) => 1, + _ => 0 +""" + +MATCH_TUPLE_BAD_PLUS_SOURCE = """fn main() -> int: + n := 1 + return match n: + (+) => 1, + _ => 0 +""" + +MATCH_TUPLE_BAD_DOUBLE_COMMA_SOURCE = """fn main() -> int: + t := (1, 2) + return match t: + (a, ,) => 1, + _ => 0 +""" + +MATCH_TUPLE_BAD_EMPTY_SOURCE = """fn main() -> int: + n := 1 + return match n: + () => 1, + _ => 0 +""" + +MATCH_TUPLE_BAD_TRAILING_SOURCE = """fn main() -> int: + n := 1 + return match n: + (a,) => 1, + _ => 0 +""" + +MATCH_TUPLE_GENERIC_SOURCE = """fn pick[T](x: T) -> int: + t := (x, 1) + return match t: + (a, b) => 1, + _ => 0 + +fn main() -> int: + return pick(9) +""" + +MATCH_TUPLE_ENUM_PAYLOAD_SOURCE = """fn main() -> int: + v := Some((1, 2)) + return match v: + Some((a, b)) => 1, + _ => 0 +""" + +MATCH_TUPLE_GENERIC_MONOMORPH_SOURCE = """fn pick[T](x: T) -> int: + t := (1, "hi") + return match t: + (n, s) =>: + if n == 1: + if s == "hi": + return 1 + return 0 + _ => 2 + +fn main() -> int: + if pick(9) != 1: + return 1 + return 42 +""" + +MATCH_ONE_CASE_SOURCE = """fn pick(n: int) -> int: + return match n: + 7 => 1, + _ => 2 + +fn main() -> int: + if pick(7) != 1: + return 1 + if pick(8) != 2: + return 2 + return 42 +""" + +# >2 non-default values, so these exercise the balanced binary search path. +# The sparse generator keeps a wide spread (step 13) to prove the tree is not +# relying on table-like density. Negative literals are not match patterns in +# this frontend, so the range starts above zero. +SPARSE_STEP = 13 +SPARSE_BASE = 3 +# Not a power of two, and dense (0..32), so the rodata jump table is selected. +DENSE_WAYS = 33 + + +def _match_values(ways, base, step): + return [base + i * step for i in range(ways)] + + +def _match_source(values, with_default=True, checker_default=False): + """Build an exhaustive checker: every probe must return its arm or the + default. `main` returns 42 only when all probes pass, so one build per -O + covers every hit plus out-of-range misses on both sides. + + `checker_default` emits a wildcard arm only to satisfy the checker's literal + exhaustiveness rule; callers strip the DEFAULT CASE row afterwards when they + need a no-DEFAULT backend image. + """ + lines = ["fn pick(n: int) -> int:", " return match n:"] + for i, value in enumerate(values): + lines.append(f" {value} => {i + 1},") + if with_default: + lines.append(" _ => 99") + elif checker_default: + lines.append(" _ => 0") + lines += ["", "fn main() -> int:"] + # Probe every arm so every tree node is exercised, plus both out-of-range + # misses when a DEFAULT exists. + probes = list(values) + if with_default: + # -1000 checks the signed jl route through the left spine. + probes += [values[0] - 1, values[-1] + 1, -1000] + for i, probe in enumerate(probes): + expected = values.index(probe) + 1 if probe in values else 99 + lines.append(f" if pick({probe}) != {expected}:") + lines.append(f" return {10 + i}") + lines.append(" return 42") + return "\n".join(lines) + "\n" + + +def _sparse_source(ways): + return _match_source(_match_values(ways, SPARSE_BASE, SPARSE_STEP)) + + +def _dense_source(ways): + return _match_source(_match_values(ways, 0, 1)) + + +def _match_plan_full(ccr_path): + """Run the read-only plan channel. + + Returns (one row per IR_MATCH, binary_emits, table_emits). Table emits is the + verification channel for the dense jump-table path, mirroring binary_emits. + """ + r = _run([COREARCH, ccr_path, "--dump-match-plan"]) + assert r.returncode == 0, f"corearch --dump-match-plan failed: {r.stdout}\n{r.stderr}" + plans = [] + binary_emits = None + table_emits = 0 + for line in r.stdout.splitlines(): + if line.startswith("match "): + parts = line.split() + assert len(parts) == 8, line + plans.append( + { + "nod": int(parts[1]), + "cases": int(parts[3]), + "nondefault": int(parts[5]), + "strategy": parts[7], + } + ) + elif line.startswith("binary_emits "): + binary_emits = int(line.split()[1]) + elif line.startswith("table_emits "): + table_emits = int(line.split()[1]) + assert binary_emits is not None, f"missing binary_emits line: {r.stdout!r}" + assert "table_emits " in r.stdout, f"missing table_emits line: {r.stdout!r}" + return plans, binary_emits, table_emits + + +def _match_plan(ccr_path): + """Backward-compatible two-value view of the plan channel.""" + plans, binary_emits, _table_emits = _match_plan_full(ccr_path) + return plans, binary_emits + + +def _ccr_file(tmp, data, name): + ccr = tmp / f"{name}.ccr" + ccr.write_bytes(data) + return ccr + + +def _run_corearch_elf(tmp, data, name, opt=2): + """Build a tagged-mode ELF straight from a .ccr image and return its rc.""" + ccr = _ccr_file(tmp, data, name) + elf = tmp / f"{name}.elf" + built = _run( + [COREARCH, ccr, "--elf", "--static", "-O", str(opt), "-o", elf] + ) + assert built.returncode == 0, ( + f"corearch --elf failed: {built.stdout}\n{built.stderr}" + ) + run = subprocess.run( + [str(elf)], capture_output=True, text=True, timeout=30, cwd=str(BASE) + ) + return run.returncode + + +def _build_corearch_elf(tmp, data, name, opt=2, raw=False): + """Build a static ELF from a .ccr and return (path, bytes).""" + ccr = _ccr_file(tmp, data, name) + elf = tmp / f"{name}.elf" + args = [COREARCH, ccr, "--elf", "--static", "-O", str(opt), "-o", elf] + if raw: + args.insert(3, "--raw-int") + built = _run(args) + assert built.returncode == 0, ( + f"corearch --elf failed: {built.stdout}\n{built.stderr}" + ) + return elf, elf.read_bytes() + + +TABLE_STUB = bytes.fromhex("4b6304934c01d8ffe0") +# lea r11, [rip+disp32] is 4c 8d 1d ; the dense table stub follows it. +TABLE_LEA_PREFIX = bytes.fromhex("4c8d1d") + + +def _table_region(data, span): + """Locate the dense table via its lea/movsxd/add/jmp stub. + + File offsets equal code-buffer positions (the code segment maps at zero + offset), so table[+entry] is the absolute target position. Returns + (lea_pos, table_pos, entries) with entries as signed 32-bit offsets. + """ + stub = data.find(TABLE_STUB) + assert stub >= 0, "no dense jump-table stub in ELF" + assert data.find(TABLE_STUB, stub + 1) < 0, "more than one table stub" + lea = stub - 7 + assert data[lea : lea + 3] == TABLE_LEA_PREFIX, data[lea : lea + 7].hex() + rel = struct.unpack_from("= 0, f"MATCH scrutinee is {rec[2]}, expected a var" + assert rec[3] == 0, f"MATCH case start is {rec[3]}, expected 0" + assert rec[4] == 5, f"MATCH case count is {rec[4]}, expected 5" + assert len(rows) == 5, f"CASE has {len(rows)} rows, expected 5" + assert [row["kind"] for row in rows] == [ + KIND_VALUE, + KIND_VALUE, + KIND_VALUE, + KIND_VALUE, + KIND_DEFAULT, + ] + assert [row["value"] for row in rows] == [10, 20, 30, 40, 0] + assert [row["str_index"] for row in rows] == [-1, -1, -1, -1, -1] + assert [row["flags"] for row in rows] == [0, 0, 0, 0, 0] + _assert_targets_are_labels(data, rows) + assert len({row["target_nod"] for row in rows}) == 5, ( + "distinct arms must target distinct body labels" + ) + _assert_program(tmp, MATCH_INT_SOURCE, 42) + + +def test_bool_match_is_one_match_with_value_cases(): + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + data = _build_ccr(tmp, MATCH_BOOL_SOURCE, "match_bool") + rec = _single_match(data) + rows = _parse_case(segment(data, CASE_TAG)) + + assert rec[1] == -1 + assert rec[2] >= 0 + assert rec[3] == 0 + assert rec[4] == 3 + assert [row["kind"] for row in rows] == [ + KIND_VALUE, + KIND_VALUE, + KIND_DEFAULT, + ] + assert [row["value"] for row in rows] == [1, 0, 0] + _assert_targets_are_labels(data, rows) + _assert_program(tmp, MATCH_BOOL_SOURCE, 42) + + +def test_enum_match_is_one_match_with_enum_cases(): + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + data = _build_ccr(tmp, MATCH_ENUM_SOURCE, "match_enum") + rec = _single_match(data) + rows = _parse_case(segment(data, CASE_TAG)) + + assert rec[1] == -1 + assert rec[2] >= 0 + assert rec[3] == 0 + assert rec[4] == 4 + assert [row["kind"] for row in rows] == [ + KIND_ENUM, + KIND_ENUM, + KIND_ENUM, + KIND_DEFAULT, + ] + enum_values = [row["value"] for row in rows[:3]] + assert all(value > 0 for value in enum_values), enum_values + assert len(set(enum_values)) == 3, enum_values + assert rows[3]["value"] == 0 + _assert_targets_are_labels(data, rows) + _assert_program(tmp, MATCH_ENUM_SOURCE, 42) + + +def _run_raw_elf(tmp, data, name): + """Build an ELF through corearch in raw-int mode and return its exit code.""" + ccr = tmp / f"{name}.ccr" + ccr.write_bytes(data) + elf = tmp / f"{name}.elf" + built = _run([COREARCH, ccr, "--elf", "--static", "--raw-int", "-o", elf]) + assert built.returncode == 0, ( + f"corearch --raw-int failed: {built.stdout}\n{built.stderr}" + ) + assert elf.exists(), "corearch --raw-int produced no ELF" + run = subprocess.run( + [str(elf)], capture_output=True, text=True, timeout=30, cwd=str(BASE) + ) + return run.returncode + + +def test_unsupported_patterns_keep_sequential_lowering(): + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + data = _build_ccr(tmp, MATCH_UNSUPPORTED_SOURCE, "match_unsupported") + assert _match_nodes(data) == [], ( + "string/ident-binding/struct patterns must not become MATCH" + ) + assert segment(data, CASE_TAG) == struct.pack("= 0 + assert rec[4] == 2 + assert [row["kind"] for row in rows] == [KIND_VALUE, KIND_VALUE] + _assert_targets_are_labels(data, rows) + + # The checker now rejects a source-level no-DEFAULT literal match, so the + # no-DEFAULT image is reached by stripping the wildcard row. A miss must + # still trap (SIGILL) on both optimization levels. + elf, _elf_data = _build_corearch_elf(tmp, data, "match_nodefault", opt=2) + run = subprocess.run( + [str(elf)], capture_output=True, text=True, timeout=30, cwd=str(BASE) + ) + assert run.returncode == -4, ( + f"ELF did not trap with SIGILL: rc={run.returncode}" + ) + + +def test_no_default_match_dispatches_hits(): + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + checked = _build_ccr( + tmp, MATCH_NO_DEFAULT_HIT_SOURCE, "match_nodefault_hit_checked" + ) + data = _drop_match_default(checked, "match_nodefault_hit") + rec = _single_match(data) + rows = _parse_case(segment(data, CASE_TAG)) + assert rec[4] == 2 + assert KIND_DEFAULT not in [row["kind"] for row in rows] + assert _run_corearch_elf(tmp, data, "match_nodefault_hit", opt=2) == 33 + + +def test_enum_match_raw_int_mode(): + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + data = _build_ccr(tmp, MATCH_ENUM_SOURCE, "match_enum_raw") + assert len(_match_nodes(data)) == 1 + assert _run_raw_elf(tmp, data, "match_enum_raw") == 42 + + +def test_loop_match_all_opt_levels(): + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + data = _build_ccr(tmp, MATCH_LOOP_SOURCE, "match_loop") + rec = _single_match(data) + rows = _parse_case(segment(data, CASE_TAG)) + assert rec[4] == 3 + assert [row["kind"] for row in rows] == [ + KIND_VALUE, + KIND_VALUE, + KIND_DEFAULT, + ] + _assert_program( + tmp, MATCH_LOOP_SOURCE, 42, opts=(0, 2, 3), name="match_loop" + ) + + +def test_ident_fallback_executes_old_path(): + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + data = _build_ccr(tmp, MATCH_IDENT_FALLBACK_SOURCE, "match_ident_fallback") + assert _match_nodes(data) == [] + assert segment(data, CASE_TAG) == struct.pack("= 2, text + + +def test_interpolated_string_pattern_rejected(): + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + _assert_compile_rejected( + tmp, + MATCH_INTERP_PATTERN_SOURCE, + "interp_pattern", + ["error[TM09]", "String interpolation is not supported"], + ) + + +def test_nested_literal_pattern_rejected(): + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + _assert_compile_rejected( + tmp, + MATCH_NESTED_LITERAL_SOURCE, + "nested_literal", + ["error[TM09]", "Nested match patterns are not supported"], + ) + + +def test_nested_composite_payload_bypass_is_rejected(): + cases = [ + ("nested_second", MATCH_NESTED_SECOND_BYPASS_SOURCE), + ("nested_first", MATCH_NESTED_FIRST_BYPASS_SOURCE), + ("nested_deep", MATCH_NESTED_DEEP_BYPASS_SOURCE), + ] + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + needles = ["error[TM09]", "Nested match patterns are not supported"] + for name, source in cases: + _assert_compile_rejected(tmp, source, name, needles) + # `run` shares the same checker; the interpreter must reject the + # pattern too, not silently return one of the divergent values. + run = _run([COREC, "run", source], cwd=BASE) + text = run.stdout + run.stderr + assert run.returncode == 1, ( + f"{name}: interpreter accepted a nested payload pattern: " + f"rc={run.returncode}: {text}" + ) + for needle in needles: + assert needle in text, f"{name}: interpreter missing {needle!r}: {text}" + + +def test_multi_payload_ident_bindings_work(): + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + data = _build_ccr( + tmp, MATCH_MULTI_PAYLOAD_BIND_SOURCE, "multi_payload_bind" + ) + assert _match_nodes(data) == [], "payload binding keeps the sequential path" + assert segment(data, CASE_TAG) == struct.pack(" int: + return match n: + 1 => match m: + 10 => 100, + _ => 101, + 2 => 2, + _ => 3 + +fn main() -> int: + if pick(1, 10) != 100: + return 1 + if pick(1, 7) != 101: + return 2 + if pick(2, 0) != 2: + return 3 + if pick(9, 0) != 3: + return 4 + return 42 +""" + +# Same function body on both sides; only the earlier pad function changes. The +# extra `x = x` adds one node but no variable, so pick_b's var baseline is +# unchanged while its node/instruction baseline shifts by one. +SHIFT_PAD_V1 = """fn pad(b: bool) -> int: + x := 1 + x = x + if b: + return 1 + return 0 +""" + +SHIFT_PAD_V2 = """fn pad(b: bool) -> int: + x := 1 + x = x + x = x + if b: + return 1 + return 0 +""" + +SHIFT_PICK = """fn pick_b(n: int) -> int: + return match n: + 7 => 70, + 8 => 80, + 9 => 90, + _ => 99 +""" + +SHIFT_MAIN = """fn main() -> int: + if pick_b(9) != 90: + return 2 + return 42 +""" + + +def _snapshot_labels(snap, data): + """Label numbers carried by this snapshot's IR_LABEL nodes.""" + labels = set() + for i in range(snap["node_count"]): + off = snap["node_p"] + i * 64 + if struct.unpack_from("= 0, f"CASE model target label {label} is unresolved" + assert label in labels, ( + f"CASE model target label {label} is not an IR_LABEL in this function" + ) + + +def _snapshot_match_nod(data, snap, ordinal=0): + """Return (local index, record offset, s2, s3) for a MATCH NOD record.""" + seen = 0 + for i in range(snap["node_count"]): + off = snap["node_p"] + i * 64 + if struct.unpack_from(" expected per-MATCH case counts. + """ + _clean_cache(tmp) + src = _write_source(tmp, source, base) + cold_elf, cold_ccr = _build_source(tmp, src, "cold", tmp) + entries = {fn: _cache_entry(tmp, fn) for fn in fns} + before = {fn: entry.read_bytes() for fn, entry in entries.items()} + stamps = {fn: _entry_stamp(entry) for fn, entry in entries.items()} + snaps = {} + for fn, counts in fns.items(): + snaps[fn] = _cir_snapshot(before[fn]) + assert snaps[fn]["name"] == fn, (snaps[fn]["name"], fn) + _assert_snapshot_case_section(snaps[fn], before[fn], counts) + case_rows = _parse_case(segment(cold_ccr.read_bytes(), CASE_TAG)) + assert len(case_rows) == sum(sum(c) for c in fns.values()), len(case_rows) + _assert_targets_are_labels(cold_ccr.read_bytes(), case_rows) + + time.sleep(0.02) + warm_elf, warm_ccr = _build_source(tmp, src, "warm", tmp) + for fn in fns: + assert _entry_stamp(entries[fn]) == stamps[fn], ( + f"{fn}: warm build did not hit the cache" + ) + assert entries[fn].read_bytes() == before[fn], f"{fn}: hit rewrote the entry" + assert cold_elf.read_bytes() == warm_elf.read_bytes(), "cold/warm ELF differ" + assert cold_ccr.read_bytes() == warm_ccr.read_bytes(), "cold/warm .ccr differ" + assert _run_elf(warm_elf) == expected, "warm ELF returned the wrong status" + return snaps + + +def _case_model_counts(stdout): + """Parse the hidden --dump-case-model lines into (before, after) dicts.""" + before = {} + after = {} + for line in stdout.splitlines(): + parts = line.split() + if len(parts) != 3: + continue + if parts[0] == "case_model_before": + before = {"model": int(parts[1]), "wire": int(parts[2])} + elif parts[0] == "case_model_after": + after = {"model": int(parts[1]), "wire": int(parts[2])} + return before, after + + +def test_case_model_is_semantic_before_lower(): + """ir_gen fills only the semantic model; ccr_lower derives the wire table. + + The hidden channel reports both counts around lower_to_ccr. Before lowering + the wire table must be empty while the model has rows; afterwards the wire + count must equal the model count and every MATCH range must index that wire + table. The warm run proves a restored model is also not pre-expanded to wire. + """ + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + _clean_cache(tmp) + src = _write_source(tmp, MATCH_INT_SOURCE, "case_model_contract") + ccr = tmp / "case_model_contract.ccr" + r = _run( + [COREC, "ccr", src, "-o", ccr, "--dump-case-model"], + cwd=tmp, + ) + assert r.returncode == 0, f"corec ccr failed: {r.stdout}\n{r.stderr}" + before, after = _case_model_counts(r.stdout) + assert before, f"missing case_model_before: {r.stdout!r}" + assert after, f"missing case_model_after: {r.stdout!r}" + assert before["model"] > 0, before + assert before["wire"] == 0, before + assert after["model"] == before["model"], (before, after) + assert after["wire"] == before["model"], (before, after) + + data = ccr.read_bytes() + validate(data) + nodes = _nod_records(data) + matches = [rec for rec in nodes if rec[0] == IR_MATCH] + assert matches, "expected at least one IR_MATCH" + for rec in matches: + start, count = rec[3], rec[4] + assert count > 0, rec + assert 0 <= start < after["wire"], (start, after) + assert start + count <= after["wire"], (start, count, after) + rows = _parse_case(segment(data, CASE_TAG)) + assert len(rows) == after["wire"], (len(rows), after) + _assert_targets_are_labels(data, rows) + + # Warm run: the cache restores the semantic model, not the wire. The + # entries must hit, the pre-lower wire count must stay zero, and the + # derived .ccr must be byte-identical. + entries = [_cache_entry(tmp, fn) for fn in ("pick", "main")] + stamps = [_entry_stamp(entry) for entry in entries] + warm_ccr = tmp / "case_model_contract_warm.ccr" + time.sleep(0.02) + rw = _run( + [COREC, "ccr", src, "-o", warm_ccr, "--dump-case-model"], + cwd=tmp, + ) + assert rw.returncode == 0, f"warm corec ccr failed: {rw.stdout}\n{rw.stderr}" + warm_before, warm_after = _case_model_counts(rw.stdout) + assert warm_before, f"missing warm case_model_before: {rw.stdout!r}" + assert warm_after, f"missing warm case_model_after: {rw.stdout!r}" + for entry, stamp in zip(entries, stamps): + assert _entry_stamp(entry) == stamp, ( + f"{entry.name}: warm run did not hit the cache" + ) + assert warm_before["model"] == before["model"], (before, warm_before) + assert warm_before["wire"] == 0, warm_before + assert warm_after["wire"] == warm_before["model"], (warm_before, warm_after) + assert warm_ccr.read_bytes() == ccr.read_bytes(), "cold/warm .ccr differ" + + +def test_cir_cache_restores_match_cases_cold_warm(): + """MATCH functions are saved, hit on the warm path, and rebuild byte-identically.""" + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + snaps = _assert_match_cold_warm( + tmp, MATCH_INT_SOURCE, "match_cache", 42, {"pick": [5]} + ) + # The old rejection path would never save this entry at all. + assert snaps["pick"]["instr_count"] > 0 + + +def test_cir_cache_two_match_functions_disjoint_ranges(): + """Two cached MATCH functions keep disjoint CASE ranges after restore.""" + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + snaps = _assert_match_cold_warm( + tmp, + MATCH_TWO_FUNCS_SOURCE, + "match_two_cache", + 42, + {"pick_a": [3], "pick_b": [4]}, + ) + rows = _parse_case(segment(Path(str(tmp / "warm.elf") + ".ccr").read_bytes(), CASE_TAG)) + assert len(rows) == 7, rows + nodes = _nod_records(Path(str(tmp / "warm.elf") + ".ccr").read_bytes()) + match_nodes = [i for i, rec in enumerate(nodes) if rec[0] == IR_MATCH] + assert len(match_nodes) == 2, match_nodes + ranges = sorted( + (nodes[m][3], nodes[m][3] + nodes[m][4]) for m in match_nodes + ) + assert ranges[0] == (0, 3), ranges + assert ranges[1] == (3, 7), ranges + assert snaps["pick_a"]["match_count"] == 1 + assert snaps["pick_b"]["match_count"] == 1 + + +def test_cir_cache_nested_match_case_order(): + """Nested matches append their range after the enclosing one on both paths.""" + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + snaps = _assert_match_cold_warm( + tmp, + MATCH_NESTED_CASE_SOURCE, + "match_nested_cache", + 42, + {"pick": [3, 2]}, + ) + nested = snaps["pick"]["matches"] + assert [m["count"] for m in nested] == [3, 2] + rows = _parse_case(segment(Path(str(tmp / "warm.elf") + ".ccr").read_bytes(), CASE_TAG)) + assert len(rows) == 5, rows + + +def test_cir_cache_warm_restore_remaps_shifted_case_targets(): + """A hit with a shifted node/instruction baseline rebuilds CASE labels. + + The fingerprints hash the whole source, so a normal warm build cannot shift + a cached function's baseline. This fixture constructs the shift explicitly: + the pad function gains one node (no variables), and the cached pick_b/main + v1 snapshots are re-labelled with their v2 fingerprints. The model stores + body label numbers, which are unchanged by the shift; ccr_lower must resolve + them to the new NOD ids so the warm wire matches the cold wire. + """ + with tempfile.TemporaryDirectory() as td: + root = Path(td) + + # Baseline: normal cold v2 build in its own directory. + base_dir = root / "base" + base_dir.mkdir() + v2_source = SHIFT_PAD_V2 + "\n" + SHIFT_PICK + "\n" + SHIFT_MAIN + base_src = _write_source(base_dir, v2_source, "shift") + base_elf, base_ccr = _build_source(base_dir, base_src, "baseline", base_dir) + base_pick = _cache_entry(base_dir, "pick_b") + base_snap = _cir_snapshot(base_pick.read_bytes()) + base_main = _cache_entry(base_dir, "main") + base_main_snap = _cir_snapshot(base_main.read_bytes()) + + # v1: smaller pad, snapshot pick_b at node_start 15. + work = root / "work" + work.mkdir() + v1_source = SHIFT_PAD_V1 + "\n" + SHIFT_PICK + "\n" + SHIFT_MAIN + work_src = _write_source(work, v1_source, "shift") + _build_source(work, work_src, "v1", work) + v1_pick = _cache_entry(work, "pick_b") + v1_bytes = bytearray(v1_pick.read_bytes()) + v1_snap = _cir_snapshot(bytes(v1_bytes)) + assert v1_snap["node_start"] + 1 == base_snap["node_start"], ( + v1_snap["node_start"], + base_snap["node_start"], + ) + assert v1_snap["var_start"] == base_snap["var_start"], ( + v1_snap["var_start"], + base_snap["var_start"], + ) + v1_main = _cache_entry(work, "main") + v1_main_bytes = bytearray(v1_main.read_bytes()) + v1_main_snap = _cir_snapshot(bytes(v1_main_bytes)) + assert v1_main_snap["node_start"] + 1 == base_main_snap["node_start"] + assert v1_main_snap["var_start"] == base_main_snap["var_start"] + + # Rewrite v2's source and seed pick_b/main with the shifted v1 snapshots + # plus the v2 fingerprints. pad misses and is regenerated. Keeping main a + # hit isolates the CASE remap from the separate cache-hit label-counter + # issue (a regenerated function after a label-using hit starts at 1). + work_src.write_text(v2_source, encoding="utf-8") + for entry, old_snap, new_snap in ( + (v1_pick, v1_snap, base_snap), + (v1_main, v1_main_snap, base_main_snap), + ): + data = bytearray(entry.read_bytes()) + struct.pack_into(" lea, (lea, table) + assert table % 4 == 0, table + assert len(set(entries)) == DENSE_WAYS, entries + for k, entry in enumerate(entries): + target = table + entry + assert 0 <= target < table, (k, entry, target, table) + assert abs(entry) < table, (k, entry) + assert elf_data[target : target + 2] == b"\x49\xba", ( + k, + target, + elf_data[target : target + 8].hex(), + ) + imm = struct.unpack_from(" int:", " return match n:"] + for value in range(8): + lines.append(f" {value} => {value + 1},") + lines += [" _ => 99", "", "fn pick_b(n: int) -> int:", " return match n:"] + for value in range(100, 116): + lines.append(f" {value} => {value - 99},") + lines += [" _ => 98", "", "fn main() -> int:"] + for value in range(8): + lines.append(f" if pick_a({value}) != {value + 1}:") + lines.append(f" return {10 + value}") + for value in range(100, 116): + lines.append(f" if pick_b({value}) != {value - 99}:") + lines.append(f" return {30 + value - 100}") + lines += [" if pick_a(50) != 99:", " return 70"] + lines += [" if pick_b(50) != 98:", " return 71"] + lines += [" return 42"] + source = "\n".join(lines) + "\n" + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + data = _build_ccr(tmp, source, "match_two_dense") + assert len(_match_nodes(data)) == 2 + plans, binary_emits, table_emits = _match_plan_full( + _ccr_file(tmp, data, "match_two_dense_plan") + ) + assert [p["strategy"] for p in plans] == ["table", "table"], plans + assert [p["nondefault"] for p in plans] == [8, 16], plans + assert binary_emits == 0 + assert table_emits == 2, table_emits + _assert_program(tmp, source, 42, opts=(0, 2, 3), name="match_two_dense") + + _elf, elf_data = _build_corearch_elf( + tmp, data, "match_two_dense_elf", opt=2 + ) + stubs = [] + pos = elf_data.find(TABLE_STUB) + while pos >= 0: + stubs.append(pos) + pos = elf_data.find(TABLE_STUB, pos + 1) + assert len(stubs) == 2, stubs + for stub, span in zip(stubs, (8, 16)): + lea = stub - 7 + assert elf_data[lea : lea + 3] == TABLE_LEA_PREFIX + rel = struct.unpack_from(" int:", " return match n:"] + for i, value in enumerate(patterns): + lines.append(f" {value} => {i + 1},") + lines.append(" _ => 99") + lines += ["", "fn main() -> int:"] + for i, value in enumerate(patterns): + lines.append(f" if pick({value} - 16) != {i + 1}:") + lines.append(f" return {10 + i}") + lines += [ + " if pick(0 - 9) != 99:", + " return 30", + " if pick(0) != 99:", + " return 31", + " if pick(0 - 1000) != 99:", + " return 32", + " return 42", + ] + source = "\n".join(lines) + "\n" + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + data = _build_ccr(tmp, source, "match_negative") + rows = _parse_case(segment(data, CASE_TAG)) + shifted = [ + ( + row["value"] - 16, + row["kind"], + row["str_index"], + row["target_nod"], + row["flags"], + ) + for row in rows + ] + neg = _replace_case(data, _pack_case(shifted)) + plans, binary_emits, table_emits = _match_plan_full( + _ccr_file(tmp, neg, "match_negative_plan") + ) + assert plans[0]["nondefault"] == 8, plans + assert plans[0]["strategy"] == "table", plans + assert binary_emits == 0 + assert table_emits == 1 + # Tagged mode represents negative ints as BigInt pointers, so the dense + # signed range is exercised in raw-int mode (the mode that stores signed + # machine words directly). + for opt in (0, 2, 3): + elf, _elf_data = _build_corearch_elf( + tmp, neg, f"match_negative_O{opt}", opt=opt, raw=True + ) + run = subprocess.run( + [str(elf)], capture_output=True, text=True, timeout=30 + ) + assert run.returncode == 42, ( + f"raw-int negative table -O{opt} returned {run.returncode}" + ) + + +def test_dense_enum_tagged_and_raw_int_table(): + variants = [f"V{i}" for i in range(10)] + lines = ["enum E:", " " + ", ".join(variants), "", "fn pick(e: E) -> int:", + " return match e:"] + for i, variant in enumerate(variants): + lines.append(f" {variant} => {i + 1},") + lines.append(" _ => 99") + lines += ["", "fn main() -> int:"] + for i, variant in enumerate(variants): + lines.append(f" if pick({variant}()) != {i + 1}:") + lines.append(f" return {10 + i}") + lines.append(" return 42") + source = "\n".join(lines) + "\n" + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + data = _build_ccr(tmp, source, "match_enum10") + rec = _single_match(data) + assert rec[4] == 11, rec[4] + plans, binary_emits, table_emits = _match_plan_full( + _ccr_file(tmp, data, "match_enum10_plan") + ) + assert plans[0]["nondefault"] == 10, plans + assert plans[0]["strategy"] == "table", plans + assert binary_emits == 0 + assert table_emits == 1 + for opt in (0, 2, 3): + elf, _elf_data = _build_corearch_elf( + tmp, data, f"match_enum10_O{opt}", opt=opt + ) + run = subprocess.run([str(elf)], capture_output=True, text=True, timeout=30) + assert run.returncode == 42, ( + f"tagged enum table -O{opt} returned {run.returncode}" + ) + raw_elf, _raw_data = _build_corearch_elf( + tmp, data, "match_enum10_raw", opt=2, raw=True + ) + run = subprocess.run([str(raw_elf)], capture_output=True, text=True, timeout=30) + assert run.returncode == 42, f"raw-int enum table returned {run.returncode}" + + +def test_no_default_dense_match_stays_binary_and_traps(): + values = list(range(16)) + hit_source = _match_source(values, with_default=False, checker_default=True) + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + checked = _build_ccr(tmp, hit_source, "match_dense_nodefault_checked") + data = _drop_match_default(checked, "match_dense_nodefault") + rec = _single_match(data) + assert rec[4] == 16, rec[4] + plans, binary_emits, table_emits = _match_plan_full( + _ccr_file(tmp, data, "match_dense_nodefault_plan") + ) + assert plans[0]["nondefault"] == 16, plans + # A dense range without DEFAULT must not select a table: holes/misses + # have no target. Binary keeps the ud2 trap. + assert plans[0]["strategy"] == "binary", plans + assert binary_emits == 1, binary_emits + assert table_emits == 0, table_emits + for opt in (0, 2, 3): + assert _run_corearch_elf( + tmp, data, f"match_dense_nodefault_O{opt}", opt + ) == 42 + + miss_lines = ["fn pick(n: int) -> int:", " return match n:"] + for i, value in enumerate(values): + miss_lines.append(f" {value} => {i + 1},") + miss_lines += [" _ => 0", "", "fn main() -> int:", " return pick(99)"] + miss_source = "\n".join(miss_lines) + "\n" + miss_checked = _build_ccr( + tmp, miss_source, "match_dense_nodefault_miss_checked" + ) + miss_data = _drop_match_default(miss_checked, "match_dense_nodefault_miss") + miss_plans, miss_binary, miss_tables = _match_plan_full( + _ccr_file(tmp, miss_data, "match_dense_nodefault_miss_plan") + ) + assert miss_plans[0]["strategy"] == "binary", miss_plans + assert miss_binary == 1 + assert miss_tables == 0 + for opt in (0, 2, 3): + elf, _elf_data = _build_corearch_elf( + tmp, miss_data, f"match_dense_nodefault_miss_O{opt}", opt=opt + ) + run = subprocess.run( + [str(elf)], capture_output=True, text=True, timeout=30 + ) + assert run.returncode == -4, ( + f"dense no-default miss at -O{opt} did not SIGILL: " + f"rc={run.returncode}" + ) + + +def test_dense_threshold_boundary(): + # 8 unique values, span exactly 16 -> table. One wider value makes span 17, + # which must fall back to the binary tree. + table_values = list(range(7)) + [15] + binary_values = list(range(7)) + [16] + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + table_source = _match_source(table_values) + table_data = _build_ccr(tmp, table_source, "match_boundary_table") + plans, binary_emits, table_emits = _match_plan_full( + _ccr_file(tmp, table_data, "match_boundary_table_plan") + ) + assert plans[0]["nondefault"] == 8, plans + assert plans[0]["strategy"] == "table", plans + assert binary_emits == 0 + assert table_emits == 1 + _assert_program( + tmp, table_source, 42, opts=(0, 2, 3), name="match_boundary_table" + ) + + binary_source = _match_source(binary_values) + binary_data = _build_ccr(tmp, binary_source, "match_boundary_binary") + plans, binary_emits, table_emits = _match_plan_full( + _ccr_file(tmp, binary_data, "match_boundary_binary_plan") + ) + assert plans[0]["nondefault"] == 8, plans + assert plans[0]["strategy"] == "binary", plans + assert binary_emits == 1, binary_emits + assert table_emits == 0, table_emits + _assert_program( + tmp, binary_source, 42, opts=(0, 2, 3), name="match_boundary_binary" + ) + + +def test_dense_table_with_bigint_string_interning(): + # Regression: IR_CONST_BIG interns a decimal string during Phase 3 via + # g2_str_off. A g2_rodata_sz()-derived table offset recorded during code + # emission then pointed into the string area and the table jump crashed. + source = """fn pick(n: int) -> int: + return match n: + 0 => 1, + 1 => 2, + 2 => 3, + 3 => 4, + 4 => 5, + 5 => 6, + 6 => 7, + 7 => 8, + _ => 99 + +fn main() -> int: + big := 1180591620717411303424 + if big == 0: + return 1 + if pick(0) != 1: + return 10 + if pick(7) != 8: + return 11 + if pick(9) != 99: + return 12 + return 42 +""" + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + data = _build_ccr(tmp, source, "match_bigint_string") + plans, binary_emits, table_emits = _match_plan_full( + _ccr_file(tmp, data, "match_bigint_string_plan") + ) + assert plans[0]["nondefault"] == 8, plans + assert plans[0]["strategy"] == "table", plans + assert binary_emits == 0 + assert table_emits == 1 + _assert_program( + tmp, source, 42, opts=(0, 2, 3), name="match_bigint_string" + ) + + # Same-function ordering: the MATCH is emitted before the BigInt literal + # is interned, so the LEA must still be patched later. + same_source = """fn same(n: int) -> int: + r := match n: + 0 => 1, + 1 => 2, + 2 => 3, + 3 => 4, + 4 => 5, + 5 => 6, + 6 => 7, + 7 => 8, + _ => 99 + big := 1180591620717411303424 + if big == 0: + return 1 + return r + +fn main() -> int: + if same(0) != 1: + return 10 + if same(7) != 8: + return 11 + if same(9) != 99: + return 12 + return 42 +""" + same_data = _build_ccr(tmp, same_source, "match_bigint_same_fn") + same_plans, same_binary, same_tables = _match_plan_full( + _ccr_file(tmp, same_data, "match_bigint_same_fn_plan") + ) + assert same_plans[0]["strategy"] == "table", same_plans + assert same_binary == 0 + assert same_tables == 1 + _assert_program( + tmp, + same_source, + 42, + opts=(0, 2, 3), + name="match_bigint_same_fn", + ) + + +def test_table_follows_strings_in_rodata(): + values = list(range(8)) + lines = ["fn pick(n: int) -> int:", " return match n:"] + for i, value in enumerate(values): + lines.append(f" {value} => {i + 1},") + lines.append(" _ => 99") + lines += [ + "", + "fn main() -> int:", + ' s := "DENSETABLESTRING"', + ' if s == "SECONDRODATASTRING":', + " return 1", + ] + for i, value in enumerate(values): + lines.append(f" if pick({value}) != {i + 1}:") + lines.append(f" return {10 + i}") + lines.append(" return 42") + source = "\n".join(lines) + "\n" + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + data = _build_ccr(tmp, source, "match_strings") + plans, _binary_emits, table_emits = _match_plan_full( + _ccr_file(tmp, data, "match_strings_plan") + ) + assert plans[0]["strategy"] == "table", plans + assert table_emits == 1 + _elf, elf_data = _build_corearch_elf(tmp, data, "match_strings_elf", opt=2) + _lea, table, entries = _table_region(elf_data, 8) + assert len(entries) == 8 + # Tables are written after every string. Confirm both string bodies end + # before the table base, and the table does not run past the file (the + # HIT pool, when present, is appended after the tables). + for text in (b"DENSETABLESTRING", b"SECONDRODATASTRING"): + pos = elf_data.find(text) + assert pos >= 0, text + end = ((pos + len(text) + 1 + 7) // 8) * 8 + assert table >= end, (text, table, end) + assert table + 8 * 4 <= len(elf_data), (table, len(elf_data)) + + +def test_project_mode_rejects_match_plan_channel(): + # The project-mode entry (.ccr -> ELF only) must keep rejecting the + # dump-match-plan debug channel; the concat entry owns it. + text = ( + BASE / "src/targets/x86_64/linux/elf/main.cr" + ).read_text(encoding="utf-8") + start = text.index("fn backend_has_extended_flags") + end = text.index("fn corearch_main") + guard = text[start:end] + assert 'cli_has("dump-match-plan")' in guard, guard + assert "ELF emission only" in text + + +def test_match_plan_threshold_keeps_1_and_2_case_linear(): + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + one = _build_ccr(tmp, MATCH_ONE_CASE_SOURCE, "match_one") + one_plans, one_emits = _match_plan( + _ccr_file(tmp, one, "match_one_plan") + ) + assert len(one_plans) == 1, one_plans + assert one_plans[0]["nondefault"] == 1, one_plans + assert one_plans[0]["strategy"] == "linear", one_plans + assert one_emits == 0, f"linear 1-case match ran binary emitter {one_emits}x" + + two = _build_ccr(tmp, MATCH_NO_DEFAULT_HIT_SOURCE, "match_two_values") + two_plans, two_emits = _match_plan( + _ccr_file(tmp, two, "match_two_values_plan") + ) + assert len(two_plans) == 1, two_plans + assert two_plans[0]["nondefault"] == 2, two_plans + assert two_plans[0]["strategy"] == "linear", two_plans + assert two_emits == 0, f"linear 2-case match ran binary emitter {two_emits}x" + + # One source, two functions: 2 values stays linear, 3 goes binary. + mixed = _build_ccr(tmp, MATCH_TWO_FUNCS_SOURCE, "match_threshold") + mixed_plans, mixed_emits = _match_plan( + _ccr_file(tmp, mixed, "match_threshold_plan") + ) + assert [p["nondefault"] for p in mixed_plans] == [2, 3], mixed_plans + assert [p["strategy"] for p in mixed_plans] == ["linear", "binary"], mixed_plans + assert mixed_emits == 1, f"binary emitter ran {mixed_emits} times" + + _assert_program(tmp, MATCH_ONE_CASE_SOURCE, 42, name="match_one") + + +def test_enum_match_uses_binary_plan(): + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + data = _build_ccr(tmp, MATCH_ENUM_SOURCE, "match_enum_plan") + plans, binary_emits = _match_plan( + _ccr_file(tmp, data, "match_enum_plan") + ) + assert len(plans) == 1, plans + assert plans[0]["nondefault"] == 3, plans + assert plans[0]["strategy"] == "binary", plans + assert binary_emits == 1, f"binary emitter ran {binary_emits} times" + _assert_program( + tmp, MATCH_ENUM_SOURCE, 42, opts=(0, 2, 3), name="match_enum_plan_run" + ) + + +def test_sparse_no_default_binary_match_traps_on_miss(): + values = _match_values(16, SPARSE_BASE, SPARSE_STEP) + hit_source = _match_source(values, with_default=False, checker_default=True) + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + checked = _build_ccr(tmp, hit_source, "match_sparse_nodefault_checked") + data = _drop_match_default(checked, "match_sparse_nodefault") + rec = _single_match(data) + assert rec[4] == 16, rec[4] + plans, binary_emits = _match_plan( + _ccr_file(tmp, data, "match_sparse_nodefault_plan") + ) + assert plans[0]["nondefault"] == 16, plans + assert plans[0]["strategy"] == "binary", plans + assert binary_emits == 1, f"binary emitter ran {binary_emits} times" + for opt in (0, 2, 3): + assert _run_corearch_elf( + tmp, data, f"match_sparse_nodefault_hit_O{opt}", opt + ) == 42 + + # The generated checker only probes arm hits; build a single-miss main + # (with a checker-satisfying wildcard) so the stripped binary tree's miss + # edge must reach ud2. + miss_lines = ["fn pick(n: int) -> int:", " return match n:"] + for i, value in enumerate(values): + miss_lines.append(f" {value} => {i + 1},") + miss_lines += [ + " _ => 0", + "", + "fn main() -> int:", + f" return pick({values[-1] + 7})", + ] + miss_source = "\n".join(miss_lines) + "\n" + miss_checked = _build_ccr( + tmp, miss_source, "match_sparse_nodefault_miss_checked" + ) + miss_data = _drop_match_default( + miss_checked, "match_sparse_nodefault_miss" + ) + plans, binary_emits = _match_plan( + _ccr_file(tmp, miss_data, "match_sparse_nodefault_miss_plan") + ) + assert plans[0]["strategy"] == "binary", plans + assert binary_emits == 1, f"binary emitter ran {binary_emits} times" + for opt in (0, 2, 3): + elf, _elf_data = _build_corearch_elf( + tmp, + miss_data, + f"match_sparse_nodefault_miss_O{opt}", + opt=opt, + ) + run = subprocess.run( + [str(elf)], capture_output=True, text=True, timeout=30, cwd=str(BASE) + ) + assert run.returncode == -4, ( + f"binary MATCH miss at -O{opt} did not trap with SIGILL: " + f"rc={run.returncode}" + ) + + +def test_duplicate_value_binary_match_keeps_first_case(): + source = """fn pick(n: int) -> int: + return match n: + 10 => 1, + 20 => 2, + 30 => 3, + 40 => 4, + _ => 5 + +fn main() -> int: + if pick(10) != 1: + return 10 + if pick(40) != 5: + return 40 + if pick(20) != 2: + return 20 + if pick(99) != 5: + return 99 + return 42 +""" + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + data = _build_ccr(tmp, source, "match_dup") + rows = _parse_case(segment(data, CASE_TAG)) + records = [ + (row["value"], row["kind"], row["str_index"], row["target_nod"], row["flags"]) + for row in rows + ] + # Turn the 40 arm into a duplicate of 10. First-case semantics keep the + # 10 arm's target, so pick(10) must stay 1 and pick(40) falls through to + # the default. A last-wins dedupe would return 4 for pick(10). + records[3] = ( + records[0][0], + records[3][1], + records[3][2], + records[3][3], + records[3][4], + ) + dup = _replace_case(data, _pack_case(records)) + plans, binary_emits = _match_plan( + _ccr_file(tmp, dup, "match_dup_plan") + ) + assert len(plans) == 1, plans + assert plans[0]["nondefault"] == 4, plans + assert plans[0]["strategy"] == "binary", plans + assert binary_emits == 1, f"binary emitter ran {binary_emits} times" + for opt in (0, 2, 3): + assert _run_corearch_elf(tmp, dup, f"match_dup_O{opt}", opt) == 42 + + +def main(): + tests = [ + value + for name, value in sorted(globals().items()) + if name.startswith("test_") and callable(value) + ] + failed = 0 + for test in tests: + try: + test() + except Exception as exc: # noqa: BLE001 - report every failure shape + failed += 1 + print(f"FAIL {test.__name__}: {exc!r}") + else: + print(f"PASS {test.__name__}") + print(f"{len(tests) - failed}/{len(tests)} passed") + return 1 if failed else 0 + + +if __name__ == "__main__": + raise SystemExit(main())