Softcode CALL_FUNC is already total (guest error strings, never ECALL_DECLINE). The residual poison was mid-run DBT failure (code buffer full, harvest miss) returning false → full AST re-run after effects. Track eval_ctx::host_ecalls on softcode host ECALLs; after entry, commit #-1 JIT POST-ENTRY FAIL instead of re-running. Document the audit in plan-softcode-post-entry-contract.md and cross-link the Lua plan.
4.9 KiB
Softcode post-entry contract (sibling of Lua #1751)
Status: audit complete 2026-07-29; residual harden shipped with this plan
Stake issue: #1791
Lua twin: plan-lua-post-entry-contract.md
One-line summary: Softcode CALL_FUNC does not soft-decline after partial work. The residual poison path was mid-run DBT infrastructure failure → jit_eval AST re-run; that is now blocked once any host ECALL has run.
0. Why this exists
The Lua campaign deleted silent whole-chunk re-run after entry. Its standing note assumed softcode had the same property only as a derived fact:
- depth / invocation watermarks run before
dbt_run(#1002); - softcode function ECALLs return
-1(continue) and put errors in guest memory rather thanECALL_DECLINE.
That left an un-audited gap: what if dbt_run itself fails after a host
ECALL already ran (pemit, setq, nested lua(), …)? Softcode
run_cached_program used to return false → jit_eval falls through to
the AST for the entire expression — a full re-run, effect-doubling
risk. Lua's arm already committed errors instead; softcode did not.
1. Non-negotiable contract (same shape as Lua)
PRE-ENTRY (nothing user-visible has run yet)
✓ refuse compile / miss cache / watermarks / oversize cargs → AST OK
POST-ENTRY (any host softcode ECALL may have run)
✓ complete successfully and harvest
✓ same error string the AST would produce from a finished ECALL
✓ CPU LIMITED / alarm as AST-shaped diagnostic
✗ silent whole-expression AST re-run after host work
Failure is allowed; retry of the whole expression after host work is the poison.
2. Audit results (2026-07-29)
| Path | Pre- or post-entry? | Behavior | Verdict |
|---|---|---|---|
| Compile refuse / no tier2 / sandbox | Pre | jit_eval → false → AST |
OK |
| Depth / invk watermarks | Pre | before dbt_run |
OK |
Oversize cargs slots |
Pre | before dbt_run |
OK |
ECALL_CALL_FUNC / CALL_INDEX |
Post (once entered) | Error strings in guest; return -1 continue |
Total — never ECALL_DECLINE |
ECALL_SETQ / SETQ_PACK |
Post | Write-through; return -1 |
Total |
ECALL_EXIT |
End | non-negative stops dbt_run with success status |
OK |
| Mid-run code buffer full | Post (if any ECALL already ran) | dbt_run → -1 → was AST re-run |
Fixed (#1791) |
Harvest fail after rc==0 |
Post if host ECALLs | was AST re-run | Fixed (#1791) |
Alarm -3 |
Post | #-1 CPU LIMITED, handled, no re-run |
OK |
Nested lua() under softcode |
Post | Lua path Phase 4; outer softcode ECALL continues | Lua contract holds |
CALL_FUNC is not the Lua bug class
Softcode never had “decline because type surprised us after the callee
ran.” Builtin and @function ECALLs always finish the host call (or write
a #-1 … string) and resume the guest. The surprise was justified.
Residual that was real
Any dbt_run status that handle_dbt_run_status does not treat as
handled (rc != 0 and not the CPU-limited arm) made softcode
run_cached_program return false. Common theoretical trigger:
DBT code buffer full mid-translation of a later block after earlier
ECALLs already executed (see also #1315 messaging). Rare under normal
load; still a contract hole.
3. Harden shipped with this plan
eval_ctx::host_ecalls— incremented inecall_invoke_fun,ecall_invoke_ufun,ECALL_SETQ,ECALL_SETQ_PACK.- Softcode arm after
dbt_run: if status is unhandled andhost_ecalls > 0, commit#-1 JIT POST-ENTRY FAIL(or keep an existing#-1…diagnostic) and return true — no AST re-run. - Same rule for harvest failure after a completed run with host work.
- Pre-entry and “no host ECALL yet” failures still return
falseso AST can answer (correct, effect-free).
Lua path unchanged (already Phase 4).
4. What we are not claiming
- Softcode JIT is free of all bugs — only this contract class.
- Code-buffer-full never happens — only that it must not double effects.
- Every mid-run failure has a forceable purity test in CI — forcing
mid-run
dbtfull from softcode is hostile to the suite; the harden is structural.
5. Residual / follow-ups
| Item | Notes |
|---|---|
| Grep for new softcode ECALL cases | Any new host side-effect ECALL must ++host_ecalls |
| Optional soak | Drive code buffer pressure under softcode with effectors; assert single delivery |
| Cross-link | Lua plan § “Standing assumption: the softcode route…” now has this sibling |
6. Checklist
- CALL_FUNC / CALL_INDEX audit (total via guest error strings)
- Identify softcode post-entry → AST re-run sites
- Harden with
host_ecalls - Plan doc + issue #1791
- CI soak under artificial code-full (optional) )