Skip to content

Fix #139 fetch arity panic; empty min/max VALUE_ERROR; Go/JS reject core-library sugar - #140

Merged
howlcipher merged 1 commit into
mainfrom
issue139-hardening
Oct 12, 2026
Merged

howlcipher merged 1 commit into
mainfrom
issue139-hardening

Conversation

@howlcipher

Copy link
Copy Markdown
Owner

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 fetch case compiled Children[1] and Children[2] without checking arity. A one-argument (fetch url) therefore panicked with index out of range [2] with length 2. The governed profile allows fetch (network is granted at run time), and the checker only checks fetch arity on the JS path, so nothing stopped it earlier. Fix: check arity first, then fail closed with fetch 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/max is now a runtime error on the interpreter, governed AST bytecode and HFIR bytecode: stderr VALUE_ERROR: min|max of empty list, exit 1. It uses the same shape as assert, stays pure sugar, and still reports no capabilities. Test: TestEmptyMinMaxIsValueError.

3. Go/JS guard: build --target go|js now rejects the #138 built-ins and nz with UNSUPPORTED_ON_TARGET: nonzero exit, no "Built" line, no files written. Detection uses the desugar temporaries, so a user variable named sum still builds. Test: TestGoJSTargetsRejectCoreLibraryAndNz.

Docs: the heldout-v1 REPORT now gives the total-token ratio (1.076× Python), states that str_len/str_slice count bytes and that sort is O(n²). Journal: docs/journals/2026-10-12_issue139_corelib_hardening.md.

Checks: gofmt -l is empty; go build/vet/test ./... and go test ./experiments/... pass; all run.sh pass (60/60 per language, 126/126, 136/136), with the results files unchanged.

Caveats:

  • The empty-list error is a coded stderr message, not a structured RuntimeError JSON.
  • The guard does not cover all/any/ver_gt or the operand-binding sugar.
  • The AST bytecode compiler still drops a fetch body argument (this predates this PR).

@howlcipher howlcipher left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 governed on (cli_app (fetch "http://x")) panics with index 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 plain ast.ReportError diagnostic, 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 the network capability, and -run-bc without a grant fails with CAPABILITY_DENIED: network.
  • Wrong type: (fetch 1 "GET") builds. There's no static type check, so it's left to runtime. Non-blocker.
  • Regression test TestIssue139FetchArityFailsClosed covers 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 go and --target js, sum, min, max, reverse, index_of, map_list and reduce (spot-checked here, plus the test's sort, filter, str_slice, count_by, nz and list_slice) all give UNSUPPORTED_ON_TARGET, exit 1, no Built, and an empty output dir. The check runs before any write. A user variable named sum still builds and runs on both targets, printing 2.
  • str_len is named in the error text but isn't rejected. It builds and runs correctly on both targets, printing 2, so that's harmless. The message is just slightly over-inclusive.
  • Stated gaps confirmed: all and ver_gt still print Built, exit 0, and emit broken code: Go gives syntax error: unexpected {, and JS emits statements in expression position. The same is true of plain if in 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 extending rejectUnsupportedTargetSugar to the __ql*/__vA* temps, and to value-position if, 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-v1 REPORT.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: build passed on 147a7a0f. Locally, gofmt -l . is empty and go 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).

@howlcipher
howlcipher marked this pull request as ready for review October 12, 2026 17:58
@howlcipher
howlcipher merged commit aef976b into main Oct 12, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Governed build of (fetch "http://x") crashes the bytecode compiler

1 participant