Repository navigation
Fix #139 fetch arity panic; empty min/max VALUE_ERROR; Go/JS reject core-library sugar - #140
Conversation
…reject core-library sugar and nz
howlcipher
left a comment
There was a problem hiding this comment.
Dev-lead review (COMMENT) — head 147a7a0f607aa6e98c242091f0555bfbd957b5e4, base facdd332
Verdict: merge-ready on content. CI build is green on this head. Okabe may undraft and squash-merge.
#139 fetch fix: verified
- Base
facdd332:build --profile governedon(cli_app (fetch "http://x"))panics withindex out of range [2] with length 2(exit 2). On head it exits 1 with no panic and no artifact, and prints{"reason":"fetch expects (fetch url method [body])",...}(a plainast.ReportErrordiagnostic, not a coded one). - 0 args, 1 arg, and 4+ args (
(fetch "u" "GET" "b" "extra")) all get the same deterministic diagnostic on exit 1. The check runs before any child is indexed. - A well-formed
(print (fetch "http://127.0.0.1:1" "GET"))still builds under governed. FETCH carries thenetworkcapability, and-run-bcwithout a grant fails withCAPABILITY_DENIED: network. - Wrong type:
(fetch 1 "GET")builds. There's no static type check, so it's left to runtime. Non-blocker. - Regression test
TestIssue139FetchArityFailsClosedcovers both profiles, no panic, no artifact, and the good case.
Arity audit: other built-ins still panic the bytecode compiler (pre-existing, identical on base)
I swept every case in internal/bytecode/bytecode.go with 0/1/2 string args, wrapped as (cli_app (X ...)) and run through build. Results on base and head are identical apart from fetch, which this PR removes.
- Governed:
read_file(0),write_file(0–1),mkdir(0),env(0),task(0),res(0–2),res_json(0–1),store_open(0, slice bounds),defun(0–1),http_server(0).(print (str_trim))also panics. - Default profile, additionally:
exec,spawn_agent(0–1),llm_generate,sql_query(0–1),db_connect(0–2),lazy_synthesize(0–2),confidence. Under governed these are rejected by the profile first. - Arity is already checked cleanly for:
str_slice,list_get,map_get,str_split,str_join,str_replace,str_contains,parse_json,encode_json,if,let, arithmetic/comparison,to_int/to_string,sleep,map_has,index_of,sum,all,ver_gt. - Repro:
echo '(cli_app (read_file))' > p.howl && howlframe build --profile governed p.howl -o o.
These are crashes on the governed safety path, but they're pre-existing and out of #139's scope. Not a blocker for this PR. Please file a follow-up issue for a generic arity guard, e.g. a table-driven min/max children check at the top of compileNode, plus a sweep test like the one above.
Empty min/max: verified
(do (print (min (list 2 7 5))) (print (min (list))) (print "after")), and the same for max, behaves identically on the interpreter (-run), governed AST bytecode (build → -run-bc) and HFIR bytecode (-compile-hfir-bc → -run-bc). stdout is 2/7, stderr is VALUE_ERROR: min|max of empty list, the exit code is 1, and after is never printed. TestEmptyMinMaxIsValueError covers all three paths. Non-empty lists still work, and string min works (a). An all-nil list (built from map_get misses) fails on all three paths with < requires numeric operands, got <nil> and <nil>, which is a TYPE_ERROR and acceptable. This is template-only, with no new capability.
Go/JS rejection: verified, with remaining gaps confirmed
- On
--target goand--target js,sum,min,max,reverse,index_of,map_listandreduce(spot-checked here, plus the test'ssort,filter,str_slice,count_by,nzandlist_slice) all giveUNSUPPORTED_ON_TARGET, exit 1, noBuilt, and an empty output dir. The check runs before any write. A user variable namedsumstill builds and runs on both targets, printing2. str_lenis named in the error text but isn't rejected. It builds and runs correctly on both targets, printing2, so that's harmless. The message is just slightly over-inclusive.- Stated gaps confirmed:
allandver_gtstill printBuilt, exit 0, and emit broken code: Go givessyntax error: unexpected {, and JS emits statements in expression position. The same is true of plainifin value position, e.g.(print (if (< "a" "b") "y" "n")). That's the same let/if-in-expression lowering gap. I didn't exercise the inline parse_json operand binding separately; I infer it's the same class. Fine as a documented follow-up, since the Go/JS targets aren't the governed path. Suggest extendingrejectUnsupportedTargetSugarto the__ql*/__vA*temps, and to value-positionif, in the next slice.
Benchmarks
go test -count=1 ./experiments/... passes: token-efficiency-v0, heldout-v0, and heldout-v1 including spec_freeze_test. The frozen hashes hold, and no task or solution files are touched. I recomputed the new REPORT.md totals line from tokens.csv. cl100k gives 2132 vs 1982 = 1.076×, JS 0.948×, Go 0.394×. o200k gives 1.078×. Accurate.
Scope, hard nos, CI
- Files:
howlframe.go,internal/bytecode/bytecode.go,internal/ast/desugar_corelib.go,issue139_corelib_hardening_cli_test.go, heldout-v1REPORT.md, and the journal. No VM, opcode or capability changes. - Hard nos hold: no
-compile-bc/HFIR default flip, #90 untouched (stays Partial), no live LLM, no H1 PASS/KILL claim. - CI:
buildpassed on147a7a0f. Locally,gofmt -l .is empty andgo test ./...exits 0 (full output saved).
Non-blocking follow-ups: generic bytecode arity guard plus a sweep test (list above); Go/JS rejection for all/any/ver_gt/operand binding/value-position if; drop str_len from the rejection message; a coded diagnostic for fetch arity (e.g. ARITY_ERROR).
Fixes #139
Base: main
facdd332. No new language features, no token claims, no production-compile-bc/HFIR flip, #90 untouched.1. #139 root cause: the AST bytecode compiler's
fetchcase compiledChildren[1]andChildren[2]without checking arity. A one-argument(fetch url)therefore panicked withindex out of range [2] with length 2. The governed profile allowsfetch(network is granted at run time), and the checker only checksfetcharity on the JS path, so nothing stopped it earlier. Fix: check arity first, then fail closed withfetch expects (fetch url method [body]), exit 1, no panic, no artifact. A well-formed(fetch url method)still builds under governed. Test:TestIssue139FetchArityFailsClosed.2. Empty
min/maxis now a runtime error on the interpreter, governed AST bytecode and HFIR bytecode: stderrVALUE_ERROR: min|max of empty list, exit 1. It uses the same shape asassert, stays pure sugar, and still reports no capabilities. Test:TestEmptyMinMaxIsValueError.3. Go/JS guard:
build --target go|jsnow rejects the #138 built-ins andnzwithUNSUPPORTED_ON_TARGET: nonzero exit, no "Built" line, no files written. Detection uses the desugar temporaries, so a user variable namedsumstill builds. Test:TestGoJSTargetsRejectCoreLibraryAndNz.Docs: the heldout-v1 REPORT now gives the total-token ratio (1.076× Python), states that
str_len/str_slicecount bytes and thatsortis O(n²). Journal:docs/journals/2026-10-12_issue139_corelib_hardening.md.Checks:
gofmt -lis empty;go build/vet/test ./...andgo test ./experiments/...pass; allrun.shpass (60/60 per language, 126/126, 136/136), with the results files unchanged.Caveats:
all/any/ver_gtor the operand-binding sugar.fetchbody argument (this predates this PR).