diff --git a/docs/journals/2026-10-12_issue141_arity_target_sweep.md b/docs/journals/2026-10-12_issue141_arity_target_sweep.md new file mode 100644 index 00000000..0ce00491 --- /dev/null +++ b/docs/journals/2026-10-12_issue141_arity_target_sweep.md @@ -0,0 +1,95 @@ +# 2026-10-12 — Issue #141: table-driven arity check and Go/JS target sweep + +## What changed + +### 1. One arity table, enforced before codegen +- `internal/construct/arity.go` declares an `Arity{Min,Max}` for every + construct-registry entry. `Variadic` means there is no maximum. 136 entries + have an arity and 18 are exempt, out of 154 registry entries. +- Each exemption carries a reason. Most are Unsupported constructs or + directives (`include`, `use`, `patch`, `with_context`) that earlier passes + consume. The rest are pure type annotations (`type_hint`, `type_hints`, + `type_param`). +- `internal/construct/arity_test.go` is the drift guard. It fails if a + registry entry has neither an arity nor an exemption. It also fails on + stale keys, malformed ranges, or an entry that has both. +- `construct.EnforceArity` runs in `howlframe.go` immediately before each of + the five `ast.Desugar` call sites. That covers every pipeline: bytecode, + HFIR bytecode, the interpreter (`-run`), `check`, and Go/JS builds. + - A wrong arity reports + `ARITY_ERROR: read_file expects 1 arg, got 0` + (or `at least N args`, or `N to M args`). + - Exit code is 1, with no panic and no artifact, under both the default + and governed profiles. +- The walk is context-aware. It skips let binding lists, defun and lambda + parameter lists, dict pairs, and circuit argument lists, and it handles + `switch` and `cond` clauses. So `(let (min 0 max 3) ...)` is not treated as + a call to `min`. +- The AST bytecode compiler keeps the same table as a backstop at the top of + `compileNode`. The #139 fetch-specific check moved onto the table: + `fetch` is `{2,3}`. +- `and`/`or` are `{2, Variadic}` because desugar already folds the n-ary + form. The bytecode `and`/`or` case now fails closed on an unfolded n-ary + node. Before this change it silently dropped every operand after the + second. + +### 2. Tests +- `TestIssue141MotokoArityRepros` covers the full repro list from the issue, + plus `fetch` with 1 arg. +- `TestIssue141EveryArityEntry` runs one too-few call and one too-many call + for every table entry (where those exist) under both profiles. Each must + give exit 1, an ARITY_ERROR, no panic and no artifact. Under the governed + profile, a PROFILE_FORBIDDEN_CONSTRUCT rejection is also accepted. +- `TestIssue141ArityFuzzNoPanic` tries arities 0..max(4, Min+1) for all 154 + registry heads, about 4.3k CLI runs. The build paths are default bytecode, + governed bytecode, `-compile-hfir-bc`, `--target=go` and `--target=js`. + `-run` is used only at rejected arities, so no program executes and there + is no network, LLM or exec. +- The fuzz test found panics, all now fixed: + - Empty `(export)` and `(module)` panicked in the checker on every path. + Both now have an arity of 1+. + - On `--target=go`, `trace`, `fuzzy_cast`, `semantic_match`, + `assert_semantic`, `web_app` with no port, and `web_app` with non-list + handlers all panicked. Their `ast.ReportError` shape checks had been + commented out. They are re-enabled, and the `semantic_match` case check + now compares against the real node type `"List"`. + +### 3. Go/JS: never a false "Built" +- `rejectUnsupportedTargetValueForms` (in `howlframe.go`, after desugar) + fails closed on any `let`, `do`, `if`, `for` or `while` used as a value on + `--target=go|js`, with `UNSUPPORTED_ON_TARGET: