tinymux/docs/plan-softcode-post-entry-contract.md

120 lines
4.9 KiB
Markdown
Raw Permalink Normal View History

# Softcode post-entry contract (sibling of Lua #1751)
**Status:** audit complete 2026-07-29; residual harden shipped with this plan
**Stake issue:** [#1791](https://github.com/brazilofmux/tinymux/issues/1791)
**Lua twin:** [`plan-lua-post-entry-contract.md`](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 than `ECALL_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)
```text
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
1. `eval_ctx::host_ecalls` — incremented in `ecall_invoke_fun`,
`ecall_invoke_ufun`, `ECALL_SETQ`, `ECALL_SETQ_PACK`.
2. Softcode arm after `dbt_run`: if status is unhandled **and**
`host_ecalls > 0`, commit `#-1 JIT POST-ENTRY FAIL` (or keep an
existing `#-1…` diagnostic) and **return true** — no AST re-run.
3. Same rule for harvest failure after a completed run with host work.
4. Pre-entry and “no host ECALL yet” failures still return `false` so
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 `dbt` full 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
- [x] CALL_FUNC / CALL_INDEX audit (total via guest error strings)
- [x] Identify softcode post-entry → AST re-run sites
- [x] Harden with `host_ecalls`
- [x] Plan doc + issue #1791
- [ ] CI soak under artificial code-full (optional)
)