Skip to content

C4a: enforce MaxCallDepth on bytecode CALL (default 1000, --max-call-depth) - #111

Merged
howlcipher merged 1 commit into
mainfrom
okabe/c4a-max-call-depth
Oct 6, 2026
Merged

howlcipher merged 1 commit into
mainfrom
okabe/c4a-max-call-depth

Conversation

@howlcipher

Copy link
Copy Markdown
Owner

Summary

Review candidate C4a only (first bullet of C4 in docs/ai-native-language-review/11-implementation-candidates.md; finding S5).

  • Bytecode CALL now enforces VMLimits.MaxCallDepth. Exceeding it fails with structured LIMIT_EXCEEDED ("opcode":"CALL","message":"call depth limit exceeded (max N)") instead of a Go fatal error: stack overflow.
  • Semantics: a ceiling of N allows exactly N active CALL frames (main is depth 0). Zero or negative fails closed. Depth unwinds on normal return, VmReturn, and propagating panics. SPAWN_AGENT children inherit the parent's active call depth.
  • DefaultLimits.MaxCallDepth goes from 128 to 1000. The field was already shared with SPAWN_AGENT nesting, so spawn nesting also defaults to 1000. The two counters are separate and use the same ceiling. Existing spawn tests (including the depth-2 guard) pass unchanged.
  • New --max-call-depth flag (default 1000) on -run-bc and howlframe run (bytecode target). Zero and negative values are rejected, the same way as --max-instructions.

Tests

  • internal/vm/call_depth_test.go: default value; unbounded recursion with a 10M budget gives LIMIT_EXCEEDED; exact boundary (5 frames pass, 6 fail); 999 frames pass under the default; sequential shallow calls under ceiling 3; zero and negative ceilings with CALL fail; zero without CALL runs; spawn inherits call depth; depth restored on every exit path.
  • howlframe_test.go TestRunBytecodeMaxCallDepthFlag: -run-bc and run with default (pass), 3 (LIMIT_EXCEEDED), 0 and -1 (rejected).
  • gofmt -l . empty, go vet ./... clean, go test ./... -count=1 green.
  • S5 probe: infinite (call f) with -run-bc --max-instructions 2000000000 now exits 1 in ~50 ms with LIMIT_EXCEEDED.

Not in this PR

  • C4b (allocation accounting against MaxMemoryBytes, --deadline, fetch/exec byte caps), C5 receipts.
  • AST interpreter (-run / run --target=interpreter) recursion is still not bounded by this limit.
  • No HFBC wire or opcode change. Production -compile-bc is not flipped, and Write flagged http_server bytecode and stop #90 stays Partial.

Journal: docs/journals/2026-10-06_c4a_max_call_depth.md

…depth)

Review C4a (S5): ordinary bytecode CALL recursion was unbounded and could
end in a Go stack overflow when the runner raised the instruction budget.
BCVM now tracks active CALL frames and fails with structured
LIMIT_EXCEEDED ("call depth limit exceeded (max N)") at the CALL that
would exceed VMLimits.MaxCallDepth. N permits exactly N active frames;
non-positive ceilings fail closed. Depth unwinds on return, VmReturn and
propagating panics; SPAWN_AGENT children inherit the active call depth.

DefaultLimits.MaxCallDepth rises 128 -> 1000. The field was already
shared with SPAWN_AGENT nesting, so spawn nesting also defaults to 1000
(separate counters, same ceiling; existing spawn tests unchanged).
`--max-call-depth` is wired on -run-bc and `run` (bytecode target),
rejecting zero/negative like --max-instructions.

No C4b (alloc/deadline/fetch/exec caps), no AST interpreter change, no
HFBC/opcode change, no production -compile-bc flip; #90 stays Partial.
Journal: docs/journals/2026-10-06_c4a_max_call_depth.md

Co-authored-by: howlcipher <howlcipher@users.noreply.github.com>

@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 COMMENT (head 8b00cd2d886eb47dd0cc0eab669f13ba081b39aa)

Verdict: merge-ready. Diff matches claimed C4a scope. CI build green on Go 1.21. Undraft + squash-merge when you are ready.

What matches

  • Bytecode CALL checks MaxCallDepth before entering; LIMIT_EXCEEDED with opcode:"CALL" and call depth limit exceeded (max N).
  • Depth increment + deferred decrement (LIFO with VmReturn recover) restores on normal return, VmReturn, and propagating panic. TestCallDepthRestoredOnAllExits covers the three exit shapes.
  • Semantics: main at 0; ceiling N allows N active CALL frames. Fail-closed for <=0 on CALL; programs without CALL still run. Boundary tests (5 pass / 6 fail, 999 under default, sequential shallow under ceiling 3) look right.
  • SPAWN_AGENT children inherit parent callDepth (no spawn-boundary reset). Spawn nesting still uses the separate spawnDepth counter against the same MaxCallDepth ceiling.
  • --max-call-depth on -run-bc and howlframe run (bytecode), default 1000, rejects zero/negative. CLI test covers both runners.
  • Docs/journal/S5 PARTIAL + C4a status note are accurate. C4b, AST -run, HFBC wire, prod -compile-bc / #90 left alone.

Shared spawn default 128→1000

Non-blocker, noted as intentional. Raising DefaultLimits.MaxCallDepth softens SPAWN nesting ~8× because the field is shared. Mitigations already in place: separate counters, shared instruction budget still bounds spawn work, existing spawn depth-guard tests pass, runner can still lower via --max-call-depth. If spawn needs a tighter ceiling later, split MaxSpawnDepth from MaxCallDepth (out of C4a).

Hard nos

Intact: no prod HFIR / -compile-bc flip, #90 stays Partial, no HFBC wire/opcode change, no C5/harness/DOM.

No blockers.

@howlcipher
howlcipher marked this pull request as ready for review October 6, 2026 12:16
@howlcipher
howlcipher merged commit cdf2e2f into main Oct 6, 2026
1 check passed
@howlcipher
howlcipher deleted the okabe/c4a-max-call-depth branch October 6, 2026 12:16
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.

1 participant