Repository navigation
Table-driven arity check + Go/JS false-Built sweep (fixes #141) - #142
Conversation
howlcipher
left a comment
There was a problem hiding this comment.
Dev-lead review (Motoko seat), COMMENT only
Head 4aaf9c8f21af1a61e84acd3273242b137c28b81d, base aef976ba. Verified locally in a scratch checkout. Base and head binaries were built side by side.
Verdict: not merge-ready yet. There is one small blocker: a Go/JS guard regression on wasm_app, described below. Everything else checks out.
Blocker: the guard rejects 3 working wasm_app examples on Go and JS
targetStatementForms lists cli_app and web_app but leaves out wasm_app. Because of that, the top-level children of wasm_app fall into the default branch and are walked as values. Every top-level if/do is then rejected.
| example | base go | base js | head |
|---|---|---|---|
| examples/wasm_app_hello.howl | Built; go build OK; ran |
Built; node runs clean |
UNSUPPORTED_ON_TARGET on both |
| examples/wasm_app_print.howl | Built; go build OK; ran |
Built; node prints 42 |
UNSUPPORTED_ON_TARGET on both |
| examples/wasm_math.howl | Built; go build OK; ran |
Built; node runs clean |
UNSUPPORTED_ON_TARGET on both |
Fix: add "wasm_app": true to targetStatementForms and add a regression test that builds these three examples for --target=go and --target=js.
Q(a): is the conservative Go/JS guard acceptable?
Yes, once wasm_app is fixed. I built every tracked .howl file for go and js with both binaries. 168 builds succeeded on base and are rejected on head. I then compiled each base artifact: go build with a replace to the base module, node --check for JS.
- 162 / 168 base artifacts were already broken. The 81 Go artifacts had syntax errors such as
unexpected {orunexpected keyword if. The 81 JS artifacts failed with errors such asUnexpected identifier '__pj1'orUnexpected token. Rejecting these turns a false "Built" into an honest error, which is the whole point. - 6 / 168 compiled and ran. All six are the
wasm_appcases above, so they are the only real regression. - On head,
all,any,ver_gtand if-as-value all exit 1 on both go and js. None printBuilt, none leave an artifact, and all giveUNSUPPORTED_ON_TARGET(malformedall/anygets its own shape error first).
Q(b): should check reject wrong arity?
Yes. Failing earlier is better. Across 316 tracked .howl files × {default, governed} (632 runs), check gave the same exit code on base and head with 0 differences, so it produces no false positives on examples, stdlib, apps or the three benchmark corpora. The error is the usual JSON shape with line and column (for example {"reason":"ARITY_ERROR: read_file expects 1 arg, got 0","line":1,"column":11}), so the LSP and other diagnostic consumers get a position. The one test change, 1-arg achieve → 2-arg, is legitimate. Base build already rejected 1-arg achieve with achieve expects (achieve target constraint), so only check was lax before.
Re-test of the #140 audit list
I ran all 29 cases with both check and build: governed read_file 0, write_file 0/1, mkdir 0, env 0, task 0, res 0/1/2, res_json 0/1, store_open 0, defun 0/1, http_server 0, (print (str_trim)); default exec, spawn_agent 0/1, llm_generate, sql_query 0/1, db_connect 0/1/2, lazy_synthesize 0/1/2, confidence. All 58 runs gave a deterministic ARITY_ERROR: <name> expects N arg(s), got M with a line and column, exit 1, no artifact and no panic. Nothing still panics.
Arity table and drift test
- The table is keyed by registry name. The drift test checks that every
construct.Table()entry has exactly one of arity or exemption (with a reason), and it rejects stale entries in either map. Probe: I temporarily added azz_fakeregistry entry, and the test failed withzz_fake has no arity; add it to arities…. I reverted it afterwards. - Small count mismatch: the test logs
registry=154 arities=136 exempt=18, but the PR text and journal say 134/20. Please fix the text (non-blocking). - Exemptions (18): all are legitimate. They are Unsupported constructs that the construct gate rejects or the Go backend shape-checks (assert_semantic, fuzzy_cast, semantic_match, trace, middleware, schema, struct, test, match, import, go_import), forms consumed before the check runs (include, use, patch, with_context), and pure annotations (type_hint, type_hints, type_param). Nothing that should have an arity is exempted.
- Spot checks all still pass: fetch with 2 or 3 args (optional body), str_slice with 2 or 3, cli_args with 0, llm_generate with 2, variadic exec/str, n-ary
and,let, andtime_nowwith 0. - The bytecode backstop in
compileNodeuses the same table. The newand/orunfolded-operand guard is a good fail-closed addition.
Fuzz test
TestIssue141ArityFuzzNoPanic enumerates arities 0..N for every registry entry. It uses no RNG, so it is deterministic by construction, and it took 2.25 s locally, which is fine for CI. The panic fixes are real: the export/module arity entries, and in gogen the ReportError calls are un-commented (it prints JSON and runs os.Exit(1)). The "LIST" → "List" typo fix in semantic_match is also real. No recover() was added. The only recover in gogen (line ~1260) predates this PR.
fetch body caveat
Confirmed. prod_flip_criteria_test.go, subtest fetch body, asserts that the artifact does not contain the body string, and OpFetch pops 2. The PR doesn't touch that file. Silently dropping a PUT/POST body is a correctness bug, so please file it as an issue. Fixing it would mean an owner-approved change to the lock or opcode. It must not be done inside a #141-style PR.
time_now on Go
(print (time_now)) on Go now emits int(time.Now().Unix()) (Unix seconds), which matches the VM and JS. Before, it produced an empty expression. Looks good.
Scope, hard nos, CI
- 10 files: the journal,
howlframe.go, gogen, bytecode,construct/arity.goand its test, two new CLI tests, and two single-line test updates. No benchmark data, docs fence or flip files are touched. - Hard nos are intact. There is no prod
-compile-bc/HFIR default flip,TestProdFlipCriteriaLockis unedited (0-line diff), #90 is untouched, and the PR makes no live-LLM or H1 claims. - CI
build: success on4aaf9c8f.gofmt -lis clean. Localgo test ./...: exit 0.
To merge
- Add
wasm_apptotargetStatementFormsand add a test that the three wasm examples still build on go and js. - (Nit) Correct the 134/20 counts to 136/18 in the PR body and journal.
Once (1) lands and CI is green, Okabe may undraft and squash-merge. File the fetch-body issue separately.
Fixes #141
Summary
internal/construct/arity.godeclares one arity for every construct-registry entry: 136 have an arity, 18 are exempt with a reason, out of 154 entries. A drift test (internal/construct/arity_test.go) makes any new built-in declare an arity.construct.EnforceArityruns before everyast.Desugarsite in the CLI, so it covers bytecode, HFIR, the interpreter,check, Go and JS.ARITY_ERROR: read_file expects 1 arg, got 0and exits 1, with no panic and no artifact, under both the default and governed profiles.fetchis {2,3}).and/ornow fails closed on an unfolded n-ary node instead of silently dropping operands.-compile-hfir-bc, Go and JS, about 4.3k runs, asserting no panic.-runis used only at rejected arities, so nothing executes.export/module(checker).trace,fuzzy_cast,semantic_match,assert_semantic, andweb_appwith no port or non-list handlers. Their shape checks had been commented out.let/do/if/for/whileused as a value on--target=go|jsis now rejected withUNSUPPORTED_ON_TARGET.if/cond, and list_get/parse_json on a computed operand.time_nowin an expression emitted nothing; it now emits Unix seconds.TestIssue141TargetNeverFalseBuiltchecks 31 probes per target: each build is either rejected or passesgo vet/node --check.Journal:
docs/journals/2026-10-12_issue141_arity_target_sweep.mdCaveats
TestProdFlipCriteriaLock/fetch_bodyand thenested_multi_effect_*ABI conformance cases, which pin the "body skipped" behavior. That is a flip-criteria decision.wasm_appis a statement form (review fix;TestIssue141WasmExamplesBuildOnGoJSbuilds the three wasm examples on Go and JS and checks they compile).Verification
gofmt -lis empty.go build/vet/test ./...andgo test ./experiments/...pass. All threerun.shpass: v0 60/60 ×4 languages, heldout-v0 howl 30/30 and others 32/32, heldout-v1 34/34 ×4.