Repository navigation
HFIR model adapter: full -compile-hfir-bc language coverage (#88) - #167
Conversation
Widen hfir-model-adapter/v1 to every node kind LowerToBytecode lowers from source, derive for/while control edges from roles, fix transport type inference, raise candidate bounds, add -hfir-adapter-roundtrip and TestModelAdapterCoversArityTable (386 corpus programs byte-identical; 124/159 arity forms covered, 35 checked exemptions). No new opcode, capability, lowering or language form.
howlcipher
left a comment
There was a problem hiding this comment.
Review of #167 at 5c59030c8dc13f2d3f782bd3db3b097ffc7419c1 (base 5009e035)
Verdict: not merge-ready yet. The code is sound, and coverage is real and well tested. But (a) the lock change loosens a criterion and needs Owner sign-off, and (b) one cheap crash fix should land here, because this PR turns the adapter into the full-language intake for model proposals.
I used a fresh clone in /workspace/hf167-* with a base worktree. Port 8080 was free, and I started no servers.
Gates (all real exit codes)
gofmt -l .is empty.go vet ./...exit 0.go test ./...exit 0.- Focused
-vrun exit 0:TestModelAdapterCoversArityTable(386 round-trip byte-identical; 124/159 covered, 35 exempt),TestProdFlipCriteriaLock,TestHFIRBenchmarkSolutionParity, all 10TestAuthority*(#163), andTestIssue141*arity/drift/fuzz. - Mutation: deleting the
regex_matchcase fromnodeRoleAlternativesmakes the coverage test fail (exit 1, 6 corpus programs:adapter rejected ... HFIR_TRANSPORT_KIND). Restored afterwards. - The diff touches no files under
benchmarks/orexperiments/, so pins, freeze and token-benchmark inputs are untouched. - No tip-lock, no live LLM, no H1 claim.
1. TestProdFlipCriteriaLock and the criteria row, line by line
| Change | Class |
|---|---|
Old loop asserted DecodeCandidate rejects defun, call, return, while, for, read_file, write_file, mkdir, exec, fetch, regex_match, html_escape, attr_escape, time_now, sleep, try. The new code asserts these 16 are members of AdapterNodeKinds(). |
Loosening (the criterion is reversed) |
match moves from the old list to the new reject list |
Neutral |
| New reject asserts for spawn_agent, spawn, grants, llm_generate, http_server, http_server_start, res, store_put, read_line, http_request | Strengthening |
Doc row: "leaves the transport allow-list on the Phase-1 adapter subset ... #88 stays Pending" becomes "widened nodeRoles to every kind LowerToBytecode lowers" |
Loosening |
| "What this spike does": drops "still rejects html_escape, attr_escape, regex_match, time_now, and sleep" and rewords the rest to "accepts exactly ... still rejects kinds the source lowerer does not lower" | Loosening (plus a wording nit: the test checks membership, not "exactly") |
The promote-blocker fence is byte-unchanged: only 2 doc hunks and 1 test hunk changed, and neither touches the fence. read_line, http_request and req_method stay exempt. req_method is rejected by kind in my probe, but it's missing from the lock's reject list (nit: add it). The flip is gated behind the new flag in -compile-hfir-bc only, *compileBc is untouched, and "#90 stays Partial" is intact. Nothing makes HFIR or the adapter the prod default.
A criterion was loosened, so Owner sign-off is needed under the "additive or strengthening only" rule. I agree with the change itself. It's the point of #88.
2. Size limits
Corpus needs, measured over 422 programs through -hfir-adapter-roundtrip:
- Max nodes: 3046 (
heldout-v2/postcf/.../w10.howl). 82 programs exceed the old 128. - Max inputs per node: 48 (
examples/repo_analyst). - Max bytes: 416,017 (
tests/test_let_chain.howl).
Boundary probes:
- 4097 nodes, 257 inputs and >1 MiB are all rejected with
HFIR_TRANSPORT_BOUNDS. - 256 inputs is accepted and fast (3 ms).
- A 4095-node linear
binarychain decodes in 90 ms. Compile takes 1.5 to 4.4 s and allocates about 4.3 GB (quadratic in lowering). There's no stack overflow, since Go stacks grow. - The ordered
?/*validator is greedy and single-pass, so there's no backtracking. 255 alternatingparam/bodyroles is rejected in about 2 ms.
Two pre-existing DoS bugs, both reproduced on 5009e035. Neither depends on the limits, but this PR widens the surface:
- [blocker, fix here] A 4-node cyclic proposal kills the process. Take
a = binary(b, b)andb = binary(a, a).validateTransportTypes.inferhas no in-progress mark and runs beforetransportCycle, so the result isfatal error: stack overflow(not recoverable). Fix: runtransportCyclebefore the type check, or mark in-progress ininfer. - [follow-up, blocks any live run] Exponential lowering on shared pure subexpressions. If each
binarylevel references the previous node twice, the cost doubles per level. At depth 16 compile takes 0.13 s and 196 MB, at 20 it takes 2.2 s and 3.1 GB, and at 22 (a 3.7 KB proposal) it hits OOM.validateNoSharedEffectsonly blocks effectful sharing. The fix is to reject pure sharing at intake, or memoize/emit locals in lowering, or cap the tree-expanded size.
Recommendation:
- Keep 4096 nodes and 1 MiB (corpus max is 3046 nodes and 416 KB).
- Lower inputs to 64 (corpus max is 48).
- Add a nesting-depth or expanded-size cap so lowering cost is bounded by input size.
3. sleep duration
- The role mapping round-trips losslessly (
duration↔ unnamed edge). - Units are milliseconds, through the same
ToInt(x) * time.Millisecondas the interpreter, so there's parity. - Negative values return immediately. Floats truncate (0.01 becomes 0).
- string and bool fail with runtime
TYPE_ERROR. The transport type check doesn't catch them (nit). - MaxInt64 overflows
time.Durationand returns immediately. This is pre-existing and the same on all paths. - 1e15 ms sleeps unbounded under
DefaultExecutionPolicy(), which has no deadline. The instruction limit doesn't bound sleeps. WithDeadlineset, the VM stops at the deadline:LIMIT_EXCEEDED ... wall-clock deadline exceededat 3.00 s, and awhile true (sleep 1)loop stops the same way. - So yes, a model-proposed sleep can hang the run unless the runner sets a deadline. Any adapter harness must set one, or clamp sleep. The interpreter's
time.Sleepignores the deadline (pre-existing).
4. Adversarial input and fuzz
Kind and role rejections:
- These are all rejected with
HFIR_TRANSPORT_KIND: spawn_agent, spawn, grants, llm_generate, http_server(_start), res, store_put/open, match, read_line, http_request, req_method, use, module, export,EXEC,execandExec. - Wrong role (
execwithargv) and awhilewithoutbodyboth giveHFIR_TRANSPORT_ROLE. forwhose body is itself givesHFIR_TRANSPORT_EFFECT.whilewhose body isprogramgivesHFIR_TRANSPORT_CYCLE. Calling an undefined function givesHFIR_BYTECODE_INVALID.
Effects never run without a grant:
- exec with a dynamically built cmd (
"/bin/" + "sh") is denied withCAPABILITY_DENIED ... process. - try/catch around a denied exec does catch
CAPABILITY_DENIED(exit 0, error JSON printed), but the command never runs. That matches sourcetry_let. - req_query outside a server gives
UNDEFINED_VAR req. returnoutside a defun compiles and exits 1 with no output (nit: better rejected at intake).
Governed path: the adapter output is byte-identical to -compile-hfir-bc on 386 programs and runs on the same VM checks, so the #163 rules apply unchanged.
Fuzz: 600 mutated proposals (kind, role, edge retarget, add/drop edge, value, literal_kind), seeded from a defun+for+try/exec+while+read_file+sleep graph:
- 0 panics, 0 effect leaks.
- 88 ran (all exit 0). The rest were rejected: ROLE 306, LITERAL 113, UNREACHABLE 40, EFFECT 17, VALUE 6, BYTECODE_INVALID 4, ENTRY 1.
- 25 mutants formed cycles and were skipped, because they would trigger the stack-overflow bug above.
5. Coverage test and type checks
The 386/386 round-trip, the 124/35 split and the mutation result are confirmed above.
Only 11 of 35 exemptions have a sample that is checked against -compile-hfir-bc. The other 24 have an empty sample and are only checked to be absent from compiled corpus programs. I spot-checked store_open, sget, http_server, res, db_connect, optimize_block, lazy_synthesize and module, and -compile-hfir-bc rejects all of them (exit 1). Nit: add samples so the claim holds for all 35.
Type fixes behave as intended:
to_int("5") < 7is accepted and printstrue.- These are still rejected: string + number,
to_string(x) < 7, string + list, bool - number.
Asks
- Owner sign-off on the lock and criteria-row loosening.
- In this PR: run the cycle check before the type check (it's a one-line move), plus a regression test for the cyclic
binarycrash. - File as a follow-up that blocks any live-model run: the exponential/quadratic lowering cost and a deadline/sleep clamp for adapter runs.
- Nits: add req_method to the lock's reject list, lower inputs to 64, reword "accepts exactly", add exemption samples, and type-check the sleep operand at intake.
Once 1 and 2 are done, I'd call it merge-ready.
… placement, sleep type, req_method lock reject, samples for every exemption
howlcipher
left a comment
There was a problem hiding this comment.
Re-review of #167 at 0d505666c7461daff6371225d05863508d403cbc
Verdict: code merge-ready; merge gated on Owner sign-off for the lock change. Every item from my last review is fixed, and I found no new blockers.
I used a fresh worktree, which I removed afterwards. Port 8080 was free and I started no servers.
Gates (real exit codes)
gofmt -l .is empty.go vet ./...exit 0.go test ./...exit 0 (75 packages ok).- Focused
-vrun exit 0:TestModelAdapterCoversArityTable(386 programs round-trip; 126/159 covered, 33 exempt),TestProdFlipCriteriaLock,TestHFIRBenchmarkSolutionParity, all 10TestAuthority*and all 5TestIssue141*. - The new hfir unit tests pass:
BinaryCycleRejectedBeforeTypes,ReturnOutsideDefunRejected,SleepDurationTypeCheckedandInputsPerNodeBound. - The diff vs
5c59030touches only 6 files and nothing underbenchmarks/orexperiments/, so pins and freeze are unchanged.
1. Cycles
All of these now return HFIR_TRANSPORT_CYCLE with no crash:
- My a↔b repro.
binaryrings of 3, 4 and 50 nodes.- Self-loops on
binaryandprogram. - A
whilewhose body or condition (throughis_nil) points back at the loop, aforwhose iterable (a list) contains the loop, and anifwhose else branch is itself. - A
letwhose value is itself. - A
defunwhose body calls with the defun as an argument. - A cycle reachable only from a dead
if falsebranch, and a fully unreachable cycle.
Fuzz: 600 mutations, cycle-forming cases included (32 of them), with a new seed and return in the base graph:
- 0 panics and 0 leaked effects (no
pwned, no passwd contents). - 88 ran. The rest were rejected: ROLE 317, LITERAL 121, CYCLE 6, EFFECT 6, RETURN 2, and so on. The other cycle-forming mutants were rejected by earlier checks first.
2. Inputs
- 64 inputs is accepted, 65 gives
HFIR_TRANSPORT_BOUNDS. - The corpus still round-trips 386/386 byte-identically.
3. Return rule
The path restriction matches the description exactly.
Accepted:
- Directly under
program, or partway through it (the statements after it are skipped). - Under
sequence, aletbody, or anifthen-branch. - Inside a
whilewithin a defun.
Rejected with HFIR_TRANSPORT_RETURN:
- In a
whileorforbody. - In a
sequenceinside awhile, anifinside awhile, or aletinside afor. - As a try expression, in a try success body, or in a
catch. - As a
printvalue, aletvalue, anifcondition, a call argument, or a return value.
Runtime, on 3 paths: -run, -compile-bc then -run-bc, and -compile-hfir-bc then -run-bc. The adapter round-trip is byte-identical to the last of these.
(do (print "before") (return 7) (print "after"))printsbeforeand exits 7 on all 3 paths.- An
ifthat returns 3 halts before a followingwrite_file, so no file is written. Exit is 3 on all paths. - An exec that is denied before a
return 0fails withCAPABILITY_DENIEDand exit 1 on all paths, so the return does not mask the denial (the denial fires first). - Receipts (
-receipt) are written on both bytecode paths when main returns early. - There is no deferred-cleanup construct in the lowered subset that a return could skip.
The rule is sound. I recommend confirming it as is. A main-level return behaves like exit with that code, and exit is already allowed anywhere. Restricting return to the final statement would add complexity without closing any authority gap.
4. Exemption samples
- Blanking
store_open's sample locally fails the test withexempt form store_open has no sample(exit 1). I reverted it afterwards. - All 33 exemptions now carry samples, and each sample is checked to use its form.
moduleandexportround-trip byte-identically through-hfir-adapter-roundtripand run (prints1, exit 0). Nit: they count as covered becauseResolveModuleserases them before HFIR, which is worth a comment in the criteria doc.
5. sleep type check
- Rejected at transport (
HFIR_TRANSPORT_TYPE): string, bool, list and dict operands, string + string, andenv. - Accepted: int, float,
1+1andto_int. - An unknown symbol is still accepted and fails at runtime. That's expected for a dynamic value.
6. Lock diff vs 5c59030
- Test: one line, which adds
"req_method"to the reject list. That's strengthening. - Doc: only the "What this spike does" sentence changed. "accepts exactly" became "carries each ... (a membership check, not an exact-set check)". That's neutral and more accurate.
- Nothing further was loosened, and the promote-blocker fence is byte-identical.
*compileBcis untouched, there's no flip, and #90 stays Partial.- The loosening from the first commit (reject becomes carry for the 16 kinds, and the criteria row) is still present, so Owner sign-off is still required.
Remaining, not blocking this PR
- Exponential lowering on shared pure subexpressions (depth 22 runs out of memory) and the quadratic cost on deep chains. Both are pre-existing and should block any live-model run until they're fixed or capped.
- Adapter runs need a wall-clock deadline or a sleep clamp.
DefaultExecutionPolicyhas no deadline.
I don't see either filed on this PR yet. Please file them as issues.
Brings the HFIR model adapter (
hfir-model-adapter/v1, #88) up to full coverage of the language that-compile-hfir-bclowers. This PR adds no new opcode, capability, grant, lowering or language form.What changed
LowerToBytecodelowers from source (hfir.AdapterNodeKinds, 49 kinds). New ones:defun/param/call/return,while,for,try/catch,read_file,write_file,mkdir,exec,fetch,parse_json,is_nil,map_keys,html_escape,attr_escape,regex_match,time_now,sleep(transport roledurationmaps to the unnamed graph edge),cli_args, andreq_query/req_header/req_path.(map_get <expr> key)now has atargetedge.?(optional) and*(repeated) suffixes. Kinds with more than one valid shape list each alternative.for/whilecontrol edges come from the validated roles, the same wayifalready worked. The schema still has no control-edge field.to_int/to_floatreturn numbers,str_splitreturns a list, and string+string is accepted.literal_kindis never carried on non-const nodes.-hfir-adapter-roundtrip(used with-compile-hfir-bc) encodes the source graph as adapter JSON, strictly decodes it and runsCompileCandidate.Coverage test
TestModelAdapterCoversArityTable:-compile-hfir-bclowers (experiments, tests, examples, apps) round-trip through the adapter to a byte-identical artifact.-compile-hfir-bc: it must not appear in a compiled corpus program, and where a sample exists, both paths must reject it. If a form becomes lowerable later, the test fails until the adapter covers it.spawn,spawn_agent,task,grants,llm_generate,confidence,lazy_synthesize, the circuit forms, the optimize forms,schema_bridge,achieve, db/store/s*forms,http_server/res/res_json/res_header/http_res,web_app/wasm_app,module/export, plusread_line,http_requestandreq_method. Those last three are on the promote-blocker fence, and lowering them would change the locked prod-flip criteria, so they stay out of scope here.Other tests
HFIR_TRANSPORT_KIND) and for the ordered role shapes.exec,fetch,read_file,write_file,mkdirandenvreached through the adapter fail closed with no grant.Invariants
TestHFIRBenchmarkSolutionParity: 0 of 268 mismatches.UNSUPPORTED_ON_TARGETbehaviour is unchanged.TestProdFlipCriteriaLockwas updated only for the model-adapter row. It now asserts that the transport carries the source-lowered kinds and still rejectsmatch,spawn_agent,grants,llm_generate,http_server,res,store_put,read_lineandhttp_request. The criteria doc row and the docs/reference "still rejects" sentences were updated to match. Historical journals were not edited.-compile-bc/HFIR flip, Write flagged http_server bytecode and stop #90 stays Partial, no tip-lock, no DOM.go test ./...exits 0 locally.TOKBENCH_PYTHON=/workspace/tokvenv/bin/python): the v2 postcf and v3 postrec reruns show no change in the results files. v3 postrec median is still 1.186 with 8 wins.The breaking changes for adapter clients (bounds and type check) are documented in
change_log.md.