Status: v2.11.0, post-2.4 language amendments are landing incrementally (the generated inventory in docs/STATS.md is the countable truth; D-009: version strings move only with the milestone). Last tagged release: 2.7.0 (2.6.0 = the W09 A6 VM-default flip, untagged; 2.7.0 = the W06 stage-2 wave-2 Result migration, tagged when the S5 release pipeline landed; 2.8.0 = the compat-r2 reliability round, tagged; 2.9.0 = the post-wave release round: #129 fold semantics + #133 overflow family + #136 hardening + unique/json_compact contracts on main; 2.9.1 = the P2+P3+P5 performance wave (tie-exact merge sort 160x @ 8k, lazy for-range, in-place string appends) + P4-safe member reads + iter.take/json_flatten_into contracts + vm_parity LOAD_SENSITIVE fix + Android lab hardening); 2.9.2 = the P6 performance wave (loop scope-env reuse with pool re-anchor, borrowed VM names, single-lookup Env::set; rebinding note aligned to the oracle same-scope re-definition rule) — m_while -29% / m_forrange -15% / m_intadd -12%; 2.9.3 = the superbench-r2 perf round (int-key map fast path gated on num_float==0, retained-bytes append charge, del-last shortcut) + the std/url UTF-8 byte law (one %XX per UTF-8 byte, multi-byte reassembly) + the range() step-wrap overflow guard (checked_add ends the loop, mirroring the P5 lazy-for law) + the W069 core-boundary freeze lint (CI-enforced keyword/mark inventory; 2.10.0 = the Bio-Deep foundation + correctness-convergence round: R0.1 Bio-Core IR lands as std/biocore.op (pure-.op serializable core state, species/reactions/interactions, append-only event trace, deterministic tick semantics, declared read/write sets, VM/tree-walk/oracle byte identity, zero engine delta) + R0.2 typed numeric arrays as std/arrays.op (28 genes, class C4, zero engine delta) + R0.9 the biological lowering + abstraction contract (docs/spec/LOWERING.md, nine abstraction classes C1-C9, one declared lowering per biological surface, scripts/check_lowering.py CI teeth) + R0.4 the numeric policy restated as contract (DETERMINISM §5: num_exact as the one shared law site, fold MUST byte-match the runtime, generated CPython referee tables) + the correctness wave (#105 REPL/DAP full bookkeeping, #115 lexer real-zero token, #116 three engine divergences converged, #117 fold/DCE bridged break/continue remap, #119 char-based distance/similar, #121 LSP didOpen spec position + ranged didChange retention, #122/#123 CLI false-green + module-cache canonicalization family, #136 residual: round() half-away-from-zero comparison form + range_seq step-0 raise, ; 2.11.0 = the correctness-convergence-2 + verification-infra round: the S0 engine-parity batch (#119.2 DAG serialization with per-node render memo + duplication budget, #119.5 json_parse duplicate keys resolve last-wins byte-matching CPython, #107 NaN map keys accumulate per the NaN-never-equals-NaN contract + unfueled scalar-miss scans charged to the step budget, #126.5 default-argument stresses propagate as catchable stresses at the call), #124 typed-gate soundness family (?? on OptionT types the OptionT itself, list methods follow the runtime, bare gene references type as Ty::Gene, del types Null, Int ** Int is exponent-literal-aware, Bool stays orderable with the VM bool-coercion arms), #106 csv spec-collusion closure (three engine/oracle collusion classes incl. sep==newline + the csv_escape CR never-firing no-op root fix), #126 package-integrity family (install warm-check verifies the pinned checksum, walk/copy refuse symlinks, corrupt lockfile dies loudly twice over, registry tie-break last-match-wins, empty-HOME guard) + fmt re-emits infinite float literals as literals (1e999, not the display word inf) + collect comprehensions over bytes yield 0..=255 + phenotype field initializers evaluate at the new site + DAP read_message 64 MiB frame armor, the fuzz epic (#138 false-pass bundle: fatal-signal family 132/133/135/137 recorded as findings, --seed 0 stream alias fixed, PR-derived reproducible seeds, --write integrity leg on a sandbox copy, the Rust leg under the same timeout contract; #49 in-process coverage-guided libFuzzer targets fuzz_parse/fuzz_check/fuzz_run with the documented fuel cap + leak policy; #48 cross-run crash-signature dedupe DB; #60 the C4 --exec leg runs --no-vm since W09 A6 made the VM the default engine), #47 the docs-sync gate is gate-window-fast again (the seven drift-checked fields are pure file walks; the informational recount no longer blocks the gate), LSP fixes (#126.17 full-document format covers the trailing newline — idempotence gated, #126.18 the ls.rs byte/char boundary confusion fixed), #125 hardening residuals + #214 the oracle container-key law (m[[1,2]] keys by value on every lane), #121 the DAP evaluate/setVariable re-entrancy guard (the evaluate response is the very next protocol message, zero hidden stops), compat-r3 the i686 fuzz wall calibrated to the platform (45s, 3x the measured x64 worst), #213 the interp-stress grind root-caused and fixed (a stressed interpolation degraded to the literal string null which made a stressed while condition truthy FOREVER, burning the 500M-step fuel pool in 12.2s — now the stress propagates like every other expression position: 12.2s -> 0.002s, 6100x), #59 the quiet-reap law (the scope-exit reap cancel is mechanical containment narrating NOTHING; the explicit cancel() builtin keeps its documented notes; the vm_parity RACY_NOTE normalization is RETIRED — scope stderr is byte-compared strictly across all four lanes), note-parity (the oracle mirrors the engine's numeric-op stress messages for / // % ** and the E2019 offending-token display via describe_tok), and the 11 Oct superbench-r3 certification (57/57 four-way checksum agreement, engine flat vs r2)). This document is the single contract implemented identically by:
| Implementation | Language | Role |
|---|---|---|
src/ + runtime/ |
Rust + C++ | Primary compiler & runtime (the real thing) |
bootstrap/oracle.py |
Python | Reference oracle for differential testing + packaging |
web/playground/ |
TypeScript/JS | Browser subset playground |
Three implementations MUST agree on every behavior below. Where they disagree, the differential test suite fails.
- Name: Operon (an operon = a cluster of genes transcribed as one unit under shared regulation).
- Extensions:
.opsource ·.cellmethylation config ·.rnaedit patches. - Design law: Total Grammar, no
.opfile is ever rejected. Every token stream parses and runs. Parse problems degrade into wobble notes (logged repairs), never errors. - Naming law: mechanism names from molecular biology only. No scientist names anywhere (code, docs, comments).
Null · Bool · Int(i64) · Float(f64) · Str · List · Map (insertion-ordered) · Gene (function/closure) · Native (builtin function).
Truthiness (Python-style): null, false, 0, 0.0, "", [], {} are falsy; all else truthy.
Equality == is deep (recurses into lists/maps). Int/Float compare numerically.
Ordering < <= > >= on numbers and strings; between incompatible types → raises Stress unfolded (catchable).
Arithmetic:
+ - *numeric (int×float → float);+on two strings concatenates;+on two lists concatenates./true division, always Float.//floor division (Int).%floored remainder, sign follows divisor;int % intreturns Int.**power, right-associative (2 ** 3 ** 2→ 512), binds tighter than unary minus on its left (-2 ** 2→ −4); int base with a non-negative int exponent → Int, otherwise Float.- Bitwise:
& | ^(and, or, xor),<< >>shifts,~bitwise not, Int operands, Int results. - Integer overflow wraps? NO, overflow raises Stress
overflow(catchable).
- Comments:
#to end of line.#!shebang allowed on line 1. - Strings:
"double"; escapes\n \t \\ \" \{ \}; interpolation"{expr}", any expression, evaluated at runtime,str()-coerced. No single-quoted strings in canonical form (a'in code is a wobble: treated as"with a note). Raw stringsr"...", content verbatim, NO escape processing, NO interpolation (newlines allowed). Multiline strings"""...""", escapes and interpolation processed, quotes ("/"") allowed inside, content verbatim (no implicit indent stripping;operon fmtmay normalize later). Byte stringsb"..."/b'...'produce the bytes type, see the W029 bytes contract below (W029 LANDED). - Byte vs char vs grapheme indexing (W28, SPEC §3), one table, both engines:
unit what it is where it applies byte one UTF-8 byte byte_width(std/unicode) reports the UTF-8 size; raw byte access arrives with the bytes type (W029)char one Unicode scalar value (Rust char/ Python str code point)len(str),s[i],s.slice(i,j),char_at(s,i),char_slice(s,i,j),ord,for c in s, ALL string indexing is char-indexed, on BOTH cores (pinned by tests/unicode.op, tests/differential/unicode.op)grapheme one user-perceived character grapheme_len(s)over a DOCUMENTED subset (no external deps): base + combining marks (U+0300–036F, U+1AB0–1AFF, U+1DC0–1DFF, U+20D0–20FF, U+FE20–FE2F), ZWJ (U+200D) glues the previous and following char, regional-indicator pairs (flags) are one cluster; a lone RI is its own clusterExample: "👨👩👧"is 5 chars / 1 grapheme;"🇺🇸"is 2 chars / 1 grapheme;"日本語"is 3 chars / 6 UTF-8 bytes / 3 graphemes.Case folding: fold_case(s)implements a documented SUBSET, ASCII A–Z, Latin-1 Supplement (À–Ö, Ø–Þ), Latin Extended-A (U+0100–U+0137 even→odd), Greek (U+0391–U+03A9, full-range monotone; no final-sigma context rule). Everything else is unchanged. The identical table logic runs in both engines (byte-identical output); full Unicode case folding / normalization is stage 2 of W28 and is NOT claimed byfold_case(it iscasefold/norm_nfc/norm_nfd, next bullet). - Normalization, full case folding, categories (W28 stage 2), generated tables, both engines:
Tables are GENERATED, never hand-typed (the W028 critique):
scripts/gen_unicode_tables.py(Python stdlib only) emitssrc/unicode_tables.rsFROM Python'sunicodedata, and the oracle calls that very same stdlib module, so the two cores agree by construction instead of by hand-copied data. The generator verifies BEFORE emitting (and refuses to write anything on mismatch): it runs the exact runtime algorithm againstunicodedataover all 1,114,112 codepoints (NFD,NFC, and the round-trip barNFC(NFD(ch)) == unicodedata NFC(ch)), a combining-mark pair sweep, a composite+mark sweep, and Hangul jamo sweeps. Unicode version: unicodedata 15.0.0 (generator header,unicode_tables::UNIDATA_VERSION, and the oracle's interpreter must be the same Python); regenerating under a newer Python re-pins both cores at once.norm_nfd(s)(canonical decomposition, UAX #15): per-codepoint canonical decompositions FULLY EXPANDED (Hangul syllables AC00–D7A3 are algorithmic, never tabulated), then ONE canonical-ordering pass over the whole sequence (each maximal run of nonzero combining class is stable-sorted by ccc). Expansions are stored already ordered per codepoint; the runtime ordering pass exists because combining runs span codepoint boundaries.norm_nfc(s)(canonical composition, UAX #15): NFD, then left-to-right composition with the blocking rule (a combining char composes with the pending starter when nothing of class ≥ its own sits between them; thelast_cc == 0admission is also what lets Hangul L+V and LV+T compose, algorithmically, under the same rule). Composition pairs were DERIVED EMPIRICALLY by the generator (every codepoint whoseNFC(NFD(c)) == ccontributes the pairs its re-composition walk needs, taken from ONE-LEVEL decompositions so multi-level forms like U+01D5 = U+00DC + U+0304 resolve through their intermediates), so Full_Composition_Exclusion is honored by construction (U+0344 never recomposes). Canonical only: compatibility decompositions are OUT of scope (norm_nfd("fi") == "fi"; ligatures, circled, superscript, CJK compatibility forms never expand — that is NFKD/NFKC territory, not claimed by stage 2). Lone surrogates cannot reach the tables (Ruststrcannot hold them).casefold(s): FULL Unicode case folding (the C+F tables, i.e.str.casefold), 1:many (ß→ss,fi→fi), NOT locale-aware (Turkish:İfolds toi+ U+0307, bytes0x69 0xCC 0x87; there is no Turkish İ→i special case), and CONTEXT-FREE: folding owns NO final-sigma rule (that rule belongs to lowercasing, not case folding), soΣ,σandςall fold toσunconditionally in every position —casefold("ΑΣ")isασ, neverασς. This is not a deviation fromstr.casefold; it ISstr.casefold(the generator proves per-char application equals whole-stringcasefold()over a deterministic sample plus every sigma context).fold_casekeeps its documented subset contract above; the two coexist.char_category(s): ONE pinned shape, string in, the two-letter general category (Lu,Ll,Nd,Lo,Mn,Zs,Cnfor unassigned, …) of its FIRST CHAR out; empty string is the soft tier (null + note). The full category table ships run-length encoded (all 30 categories over the whole codepoint space). Error shape: a non-string argument to any of the four is the standard catchableunfoldedtype Stress ("norm_nfc(s) needs a string","norm_nfd(s) needs a string","casefold(s) needs a string","char_category(s) needs a string"), the same family aschar_at's index Stress. Achar_decomposehelper was considered and dropped:char_codes(norm_nfd(s))(std/unicode) already expresses it, the surface stays small. Pins:tests/unicode_depth.op+tests/differential/unicode_depth.op(byte-identical); proof corpus: the generator's printed verification run. - The W029 bytes contract.
bytesis a first-class immutable value kind (RustRc<Vec<u8>>, Pythonbytes): literalsb"..."/b'...'(a'form is a wobble-repair with a note), escapes\n \t \r \\ \" \' \0 \xNN(exactly two hex digits; a malformed\xis kept verbatim with a note), NO interpolation ever, non-ASCII source chars UTF-8-encoded with a note (Total Grammar: repair, never reject).type()→bytes; truthiness follows emptiness; equality is byte content;len()is the BYTE count. Indexingb[i](negative from the end) yields the byte as anint0–255; out-of-range is catchablemissing.b.slice(a, b)uses Python-style clamped negatives and returns NEW bytes, bytes have NO mutators by design.+concatenates bytes (512 MiB per-op ceiling,"bytes concat exceeds the 512 MiB ceiling"),*repeats (same ceiling family); bytes and str NEVER coerce into each other (b"x" + "y"is catchableunfolded). Iteration (for x in b) yields ints 0–255. Bytes are legal SCALAR map keys (content equality, exact on both engines). Conversions are UTF-8-only with the encoding named explicitly:bytes_from_str(s, enc?)encodes infallibly (an Operon str IS valid UTF-8; unknown encoding names areunfolded),str_from_bytes(b, enc?)decodes and reports invalid UTF-8 as the SOFT tier (null + note naming the first bad byte),bytes_from_list/bytes_to_listbridge int lists (out-of-range values areunfolded). JSON view: bytes serialize as the lossless int list (json_str(b"AB")→[65,66]); JSON has no bytes type. File I/O mirrors the text forms' armor exactly (regular-files-only, charge-before-read, TOCTOU-verified handle, hardlink defense, same capability gates):read_file_bytes(path)/write_file_bytes(path, b). Bytes cross the spawn membrane losslessly (SendValue::Bytes). The whole contract is pinned bytests/bytes.op+tests/differential/bytes.op(byte-identical) and attacked bytests/redteam/rt_p19a_bytes.op(ceilings, monotonic aggregate interaction, coercion, domain, index, capability gates). - Identifiers
[A-Za-z_][A-Za-z0-9_]*. - Numbers:
42,3.14,1e3(float). Negative via unary minus. Radix forms0xFF,0b101010,0o755(case-insensitive prefix; canonical value is the same Int)._digit separators allowed inside any numeric literal (1_000_000,0xFF_FF,1_000.5) and are stripped before parsing, the printed value is unaffected (canonical form stays decimal). A radix prefix with no valid digit after it lexes as decimal0followed by identifiers (0x→0,x). Out-of-range literals keep the existing saturate-to-0 note contract (f0fe2ec). An_in the EXPONENT (1e1_0→ 10) is accepted and skipped by all three implementations identically (both cores + oracle; #125.8 spec-text gap closed — behavior unchanged, now written down). - Newlines terminate statements;
;allowed and ignored (also;;, stray). Blocks are{ ... }. - Keywords (canonical, the parser's reserved set; the generated inventory with per-keyword programmer analogies lives in docs/KEYWORDS.md and is never hand-typed here).
- Literal words
true false nulland the logical wordsand or notare recognized in expression positions (not part of the reserved keyword table). - Marks:
@acetylate@methylate@m6a@copies@riboswitch@burst@deprecated. The six declaration marks of the biology layer: lexical presence here, attachment in §7, semantics in §11 (part contract §11a). The seventh mark is the language-surface deprecation mark (W64, below). @deprecated("migration text", since="2.4")(W64) marks a gene declaration as deprecated. The mark rides the gene as pure metadata: the interpreter never reads it, a marked gene runs, composes and dispatches exactly like any other gene (tests/deprecate.oppins runtime neutrality), and no differential parity is claimed (the oracle consumes the mark and drops it, like doc comments). The tooling surface owns the behavior:operon checkreports adeprecated-usewarning (stable codeW12) at every static free-gene call site of a marked gene in the same file, carrying the migration text and thesincegate;--strictescalates it per the strict table;// allow: deprecated-useon the call line silences it. Method calls, dynamic dispatch through a bare gene reference, and cross-file uses are the documented escape hatches (the check is file-local by design). Malformed payloads (@deprecated(42), missing message) degrade to a rung-4 note and drop the mark, never a rejection.fmtre-emits the mark canonically andfmt∘fmt = fmtholds (tests/deprecate_lint.rspins the warning, the escapes and the fmt fixpoint). Removal scheduling follows the compatibility ladder (docs/specs/COMPATIBILITY.md, W63). Honest inventory: NO core or std gene carries the mark today (constis live semantics per W05, its migration was retired), the machinery is future-proofing.#starts a line comment;##starts a doc comment (W074): consecutive##lines form a doc block that attaches to the declaration (gene/sequence/phenotype/splice/fate, including@marklines above them) it immediately hugs, the doc's last line is exactly one line above the declaration keyword line. A doc block at the very top of the file that is separated from everything by a blank line becomes the module doc. Doc text is captured verbatim after the##marker (one leading space stripped). Docs are pure metadata: they never affect evaluation, repair rungs, or differential parity, the Python oracle ignores them as comments, and parity is not required for doc content (tests/differential/doc_comments.oppins neutrality). Docs survivefmtbyte-exact (fmt∘fmt = fmtholds corpus-wide), surface in LSP hover and REPL:doc name, and are rendered byoperon doc(W073).#inside a string does NOT start a comment.
Every parse passes down the ladder; each rung below 1 emits notes (structured: line, rung, message, repair):
- Canonical, exact keyword/grammar match. No notes.
- Synonym, a known synonym table maps alternate spellings to canonical keywords:
fn func def fun sub lambda proc → gene·print echo say show → promote(promote is builtin, synonyms map in parser to apromotecall) ·var val → let·elseif → elif·foreach each → for·import include require → use·ret → return·stop → break·continue next skip → continue·yes on → true·no off → false·null nil nothing → null·&& → and·|| → or·! → not. (W06/D-014:nonewas RETIRED from the null synonyms, it is now the Option constructornone(); barenonedegrades to an unbound-ident note, never a silent null. W05:constwas RETIRED from the let synonyms, it is its own immutable-binding form, §7d.) - Wobble, an identifier within edit distance ≤ 2 of exactly one keyword (≤ 1 if its length ≤ 4) is repaired to that keyword, with a note. Applies to marks too (
@acetylat→@acetylate). Ambiguity (two keywords equidistant) → rung 4 for that token. - Semantic fallback, unknown bare identifier in expression position becomes the string literal of its own name + note ("unbound wobble"); stray tokens are skipped with notes; unclosed braces are auto-closed at EOF with notes; extra closers are skipped with notes; an unclosed string consumes to EOF with a note.
Runtime Total Grammar: reading an unbound variable → Null + note; calling a non-gene → Null + note. A run never aborts on soft failures, only exit() or an uncaught raise-style hard event ends it early (and even that prints a containment note first).
Unbound/undefined gene calls are reported by operon check as phantom calls (they run as Null at runtime).
let name = expr # definition (re-let with note "rebinding")
let [a, b] = expr # destructuring definition (v2.3), list
# pattern; *rest captures the tail; strings
# destructure by char; patterns nest; soft
# miss → null + note (never a hard failure)
let {x, y} = expr # map pattern (v2.3): each name reads that
# key (bare names only; missing → null+note)
let a, b = e1, e2 # multi-define (v2.3): every RHS is
# evaluated before any name binds
name = expr # assign (auto-let at top scope with note)
a, b = b, a # multi-assign / swap (v2.3): all RHS
# evaluated (left→right) first, then targets
# written in order; targets may be names,
# a[i] or a.k; shorter RHS → null + note
name += -= *= //= expr # compound (also on a[i], a.k targets)
if expr { } elif expr { } else { } # elif/else optional, elif chainable
while expr { }
loop { } # infinite; break/continue
scope { } # structured concurrency (§13): spawns
# inside are joined at block exit
for name in expr { } # List→items, Str→1-char strs, Map→keys,
# sequence → pulled values (§7b)
for [k, v] in expr { } # destructuring loop (v2.3), any pattern
# accepted; each item binds the pattern in a
# fresh child scope
return expr? # bare return → null
break / continue
match expr { case p { } ... } # patterns (W02 match-v2, see §5a). First
# match wins; a shape that cannot match
# falls through, matching never rejects a
# run (Total Grammar, §4)
gene add(a: Int, b: Int) -> Int # TYPED-MODE annotations, generics
# (list[T]), bounds (T: numeric), traits
# as types; opt-in static gate, §16a
use path (as name)? # file import → binds module Map (see §8)
raise expr # raise Stress{kind:"unfolded", message: str(e)}
raise kind , expr # kind ∈ unfolded missing overflow burned
stress (kind)? { B } rescue (e)? { R } # containment, see §9 (kinds incl.
# `interference`, §9b, sandbox denials)
Name { B } # bare-name block: a gene definition with no
# params (rung-4 note), `main { }` is the
# C-like entry idiom
Pattern soft-miss law (v2.3): a destructuring pattern never hard-fails a
run. Destructuring a non-container (or null) binds all its names to null with
one note; a missing element/key binds null for that piece only; extra elements
are dropped with a note. *rest on an exhausted list binds [].
match arms are patterns p, tried top to bottom; the first arm whose
pattern hits executes its body in a fresh child scope holding the pattern's
captures, and the statement ends. A pattern that cannot match (wrong tag,
wrong length, missing key, non-container subject) simply misses, it never
fails the run; with no hit and no catch-all the match falls through
silently. New in W02 (all mirrored op-for-op by the Python oracle, and by
operon fmt roundtrip):
- Variant patterns,
Some(p),None,Ok(p),Err(p)wherepis itself a pattern (nestable:Some(Some(y))). The tag IS the contract (§9): aSomepattern never matches anOkvalue. The bare tag form (Some) matches the tag with any payload;Some(p)against the payload-lessNonevalue misses. Tags are the four built-in variant constructors only, any other capitalized name in pattern position is a note + a whole-subject binding (a parenthesized payload is consumed and ignored so the token stream stays aligned). - List patterns,
[p1, p2, *rest]. Element patterns nest; the subject must be a List of exactly that length, or (with*rest) at least that length;restbinds the remaining tail as a fresh List.[]matches only the empty list. - Map patterns,
{k1, k2: p}. Each named key must be present on the subject (a Map); the bare-key form binds the value, thek: pform runs the sub-pattern against it. Non-string keys are only reachable via indexing, not patterns. - Or-patterns,
p1 | p2 | ...; alternatives are tried in order and the FIRST hitting alternative provides the bindings (an alternative that hits without binding yields unbound-name lookups downstream, soft nulls). A failed alternative's partial captures never leak into the arm. Newlines are allowed before any|(multi-line chains). - Guards,
p if cond: the condition evaluates afterp's captures, in the arm scope, so it sees the bindings (including after an or-chain, the winning alternative's). Guard false ⇒ the arm misses and matching continues. A stress inside the guard is contained to a note + miss; the ONE escape is?!propagation (§9), which is a return, never a failure, it leaves the match and unwinds to the gene boundary. - Legacy forms, unchanged, literal patterns (
1,"s",true,null), identifier binds,_, and the literal comma-run (case 1, 2= matches either). Pattern literal expressions cannot call or reference names (they are literals by construction), so guards are the supported way to test computed conditions.
Unreachable-arm report (W002 stage 2, check-side). The static engine
(src/lint.rs, rule unreachable-match-arm, stable code W06) reports an
arm that can never run under first-match-wins: an arm placed after an
UNGUARDED catch-all (_, a binding pattern, or an or-pattern containing
either; the legacy comma-run holds plain literals only, so it can never
hide a catch-all), or after a strictly broader arm whose shape provably
covers every value the later arm can match (Some(_) before Some(x),
the bare tag before a payload arm, [a, b] before [1, 2], [1, *t]
before [1, 2], {x} before {x: 1}, a literal inside an earlier
or-/comma-run, nested payloads likewise). The report is a semantic WARNING
on the lint stream (operon lint; check --style inlines it), advisory
like every lint rule: Total Grammar never rejects a program, and the
interpreter and the Python oracle are untouched, so runtime semantics and
differential parity cannot drift from this check. Conservatism is the
design bar (zero false positives): a GUARD on the earlier arm makes it
fallible, so a guarded arm is never treated as covering anything, and a
guard on the later (dead) arm does not save it, because first-match-wins
means that guard can never evaluate; structural coverage is claimed only
where the pattern algebra proves it (floats are excluded entirely, NaN
never deep-equals itself; byte-string keys compare per byte, so distinct
byte strings are never conflated; identical plain literal arms are left to
the separate duplicate-match-arm rule, W05, which owns that symptom);
or-alternatives must ALL be covered before the arm is reported. Findings
carry file and line (the first line-bearing statement of the arm body;
the AST keeps statement spans only on expression statements, so a column
is not reported, and an arm whose body opens with spanless statements,
a plain assignment say, anchors at the file's line 1, where only a
line-1 allow comment can suppress it). Exhaustiveness
(is some value class unhandled?) is intentionally NOT claimed here: that
needs the full pattern algebra and stays with the semantic lane.
cond ? a : b, ternary, right-associative, lowest precedenceor(||), short-circuits, returns operand value (a or b→ a if truthy else b)and(&&), same semantics 3.5a ?? b(v2.3), null coalescing: sits betweenorandand(a or b ?? creadsa or (b ?? c)); short-circuits; coalesces Null only, falsy-but-non-null values (0,"",[],false) pass through.- unary
not(!),not a == bparses asnot (a == b) - comparisons
== != < <= > >=andin(left-assoc, no chaining);x in xs: List membership, Str substring, Map key membership - bitwise OR
| - bitwise XOR
^ - bitwise AND
& - shifts
<< >> + -* / // %- unary
-and~(bitwise not) **power, right-associative, binds tighter than unary minus on its left- postfix: call
f(x), indexa[i], membera.k, method calla.k(args), the propagation forme?!(§9, D-014, binds tighter than every binary/ ternary operator, repeats compose), and the null-safe formsa?.k,a?.k(args)(v2.3): a Null receiver yields Null silently, no note; a non-Null receiver behaves exactly like.(missing keys still note). Chains compose:a?.b?.c. - primary: literal, ident,
(expr), list[a, b], map{k: v, "k2": v}, lambda,collect(§7),new Name(args)(§7a)
Member access on Map → key lookup (missing → Null + note). Methods (see §10) are native.
a.k = v assigns map key. a[i] = v assigns list index (out of range → Stress missing, catchable).
marks* gene name(p1: T, p2: T = default) -> T guard (cond) else { B } { body }
marks* gene (p1) => expr # anonymous lambda
marks* gene (p1) { body } # anonymous block lambda
let f = gene (x) => x * 2
marks=@acetylate | @methylate | @m6a | @copies n(§11). Sequence definitions gate at CREATION like gene calls (§11 gate order), a silenced or vetoed sequence returns null instead of starting its worker (reg-r4).guard(uORF leading guard) sits between signature and body. Ifcondfalse: runB; ifBreturned a value other than null that value is the call result; otherwise result is Null + note "guard tripped".- Closures capture the defining environment (by reference, like Python).
- Calls: positional only + defaults. Extra args → note, ignored. Missing args without default → Null + note (never a crash).
- Recursion allowed. Depth limit 10_000 → Stress
overflow.
collect expression (comprehension): for x in xs collect x * 2 is an expression evaluating to a List. Optional filter: for x in xs if x > 0 collect x.
phenotype Name {
let field = value # field with a default value (any expr)
gene init(p1, ...) { self.f = p1 } # constructor (optional)
gene method(a) { return self.f + a } # methods read/write fields via self
}
phenotype Child from Parent { ... } # inheritance: methods + fields
let o = new Name(args) # construct (calls init if declared)
new Name(args)builds an instance: field defaults apply lineage root-first (parent fields, then own overrides), then exactly oneinitruns with the args, the lineage is searched root-first and the first (least-derived)initfound wins; a child'sinitonly runs if no ancestor declares one. Without anyinit, fields keep their defaults. (superis not a binding: an unboundsuper.init(...)insideinitreads null with a note.)self.freads and writes fields inside methods. Field access from outside:o.f. Missing fields →null+ note.- Method dispatch: own methods first, then the parent chain. Missing method →
null+ note. type(o)returns the phenotype name ("Counter");o.f = vassigns a field. Phenotype instances crossspawnboundaries by serialization (they travel as maps carrying a hidden#phenotypekey).- Instance equality (dev1 ruling, closes the W34-stage-2 parity finding):
==on two instances istrueiff the class names are equal AND every field deep-equals (a == ais alwaystrue; subclasses are distinct values, the class name is part of the value). Instances are DATA; genes (closures) and sequences stay identity-based, they are behavior handles. Instance-keyed maps (m[a] = v,m.get(b)) use the same deep equality in both implementations. Pins:tests/pheno_equality.op+ byte-identicaltests/differential/pheno_equality.op(mutual-instance cycles stay cycle-safe). - Marks may precede
geneinside a phenotype (@acetylate gene m() { ... }).
sequence name(p1, p2 = default) {
...
yield v # produce a value; body suspends until pulled
}
let s = name(3)
s.next() # next value, or null when exhausted
s.collect() # drain remaining values into a List
for v in name(3) { ... } # sequences are directly iterable
- A sequence body runs on its own worker cell (a real OS thread); the consumer pulls values through a rendezvous channel. Pull is lazy, a sequence that yields forever is legal and only produces values on demand.
- Values cross the membrane by serialization. Named genes and lambdas may travel as arguments (they cross by definition); a running sequence object itself does not cross.
- Honesty note: the Rust core pulls lazily; the Python oracle models a sequence by running the body to completion on first pull (buffered). Output is identical for programs that do not print inside a sequence body, and the differential corpus avoids infinite sequences.
yieldoutside a sequence is treated asreturnwith a note.
Annotations document and enforce contracts at the boundaries, in the spirit of mypy/TypeScript-gradual-typing rather than a static checker:
gene clip(x: int, lo: int, hi: int) -> int { ... } # param + return annotations
let ratio: float = 0.5 # annotated definition
let name: str? = null # optional (accepts null)
gene handle(v: int | str) -> any { ... } # union
- Grammar: a type is a name, a name with
?(optional), or a|union of those. Known names match by value type:int float str bool list map gene sequence phenotype option result. An unknown name parses fine and matches nothing today (user-defined phenotype annotations are a later stage), never a parse rejection (Total Grammar, §4). - Widening rule:
floataccepts int (safe numeric widening);intREFUSES float, no silent narrowing.anyaccepts everything.option/resultmatch the variant FAMILIES (Some/None vs Ok/Err, the tag is the contract, §9); families are distinct. - Enforcement is a SOFT contract: violations raise catchable Stress
kind
unfolded(§9), never a hard failure and never a parse rejection.- Params are checked at the call funnel:
argument 'x' for gene 'g' expects int, got str. Default-value expressions are checked the same way. Phenotype methods run the same checks (...for method 'm'...). - Returns are checked when the gene produces its value, including a
?!-propagated variant and a guard-branch return:return of gene 'g' expects int, got str. Falling off the end without a return is an implicit null and violates a non-optional annotation:...got null (no return statement ran); declare-> T?when null is legitimate. let x: T = echecks at binding:type annotation violated: 'x' expects int, got str. On a mismatch the binding does NOT happen.
- Params are checked at the call funnel:
- Staged model (stage 1 = runtime contracts, this section; stage 2 =
check-time layer — LANDED): annotations are runtime contracts on gene
calls and definitions, and the static check-time layer has landed:
operon check --typedreports the T01..T10 finding classes (unknown member, binding/return/argument mismatch, unknown-name note, ...), type aliases (type UserId = int) resolve into every law below, and gene type params parse and check. TYPE-SYSTEM.md (docs/specs/) is normative for every type surface; the dynamic side stays frozen (a typed finding never changes whatrundoes). Known open: typed collections sugar —List<T>parses but list-literal inference still degrades conservatively; sequencing is tracked in ROADMAP-100. - W026, the collections sugar first slice: the typed-collections
layer lives as pure
.opstd modules over map/list (std/set,std/deque,std/heap,std/graph), andstd/graph.opis the first std module whose public genes carry these stage-1 annotations (the W003 sugar slice; set/deque/heap stay untyped until the mechanical pass lands). Its determinism contract is the pinned node order: node order IS thenodesmap's insertion order (both cores keep map insertion order, §19a), every traversal and tie-break follows it, ids are never sorted; pinned bytests/std_graph.opand byte-exactly bytests/differential/graph.op.
const NAME = expr; binds a name immutably. Two walls, one stress kind:
const K = 10 # immutable binding
const L = [1, 2, [3]] # the value is DEEP-FROZEN
const M = {"a": 1} # maps freeze the same way
K = 5 # frozen stress: cannot reassign const 'K'
push(L, 9) # frozen stress: cannot modify frozen list
L[0] = 9 # frozen stress: cannot modify frozen list
M["b"] = 2 # frozen stress: cannot modify frozen map
del(M, "a") # frozen stress: cannot modify frozen map
L.push(4) / M.del("a") # method forms stress identically
- Deep freeze: freezing walks the value, every list and map reachable from it (through nesting, aliases, variant payloads, and phenotype field values) becomes immutable for the rest of the run. Cycle-safe (a self-referencing container freezes once). The freeze itself draws nothing: the entropy stream is untouched (byte-identical legacy).
- Catchable, never fatal: every wall raises Stress kind
frozen(§9),rescuehandles it like any other stress.--strictruns andoperon lintsurface statically-detectable reassignments first (const-reassignfinding; name-based, conservative, anyletre-binding of the name retires the finding). - Scope of the walls: the NAME wall covers
=,op=, and multi- assign rebinding; a re-DEFINITION (const x = 1; const x = 2) rebinds likeletdoes (with the usual rebinding note). The CONTAINER wall covers index/member writes,push/pop/insert/remove/del, and their method forms. Phenotype member stores stay mutable (methods keep working); lists/maps held in fields are frozen.sort/reverse/ concatenation return NEW containers, they are reads, not writes. - The spawn boundary: frozen-ness does not cross
spawn(§13), a worker receives serialized copies, and mutating a frozen argument inside the worker body is engine-defined territory the corpus pins around (the oracle parks the reachable frozen ids for the inline body to mirror the worker's fresh-container semantics). - Back-compat: programs whose
constbindings are never reassigned and whose containers are never mutated run byte-identically to the oldconst → letsynonym (freeze draws nothing; corpus-verified). Theoperon fixconst→let migration is RETIRED (red-main r5 hotfix, W05):constis live semantics, a fixer rewrite toletwould unfreeze bindings and change program meaning (fix_corpus law 1). Theconst_to_letreport field stays (always 0) for--jsonshape stability. let mut xparses: themutannotation is documentation-only in v2.x (contextually consumed when followed by the binding name, a variable literally namedmutkeeps working).
use path;, path likestd/bio,./util,util(.opappended if absent). Binds one name: basename, oras name.use std/bio as b;→b.some_fn(...). Importing the same file twice executes it once (module cache).- Namespace spellings (W25).
::is the Rust-style separator,use std::bio;,use bio::sequence, and multi-segmentuse a::b::care EXACT sugar for the/form (a::b::cresolves asa/b/c); every separator (/,.,-,::) may be mixed within one path. The imported module is a Map, so qualified access (bio.some_fn(...)) is the namespace read, and, because theusealso flat-binds every export beside the alias, the old flat spelling (some_fn(...)) keeps working beside it (back-compat is a PINNED behavior, not an accident: tests/differential/namespaces.op pins both spellings byte-identically on both engines, plus aliased namespaces where the same module is reachable under two binding names). Stage 2 (W025) extends the surface three ways, all mirrored op-for-op by the oracle and pinned byte-identically by tests/differential/namespaces2.op: (1) nested declarations: a module file (or any block) may declaremodule name { ... }; the body runs ONCE in a fresh child scope and its names become the export table (sorted like the file-module loader sorts them; data lets export beside genes; a gene in the table closes over the table's own scope).moduleis a CONTEXTUAL word: onlymodule NAME {is a declaration, any other use of the identifier parses exactly as before, so no pre-existing program changes meaning. Last declaration wins: a latermodule name(or gene) re-binds the name. (2) path descent: a multi-segmentusestays FILE-FIRST (W069); when the full path is not a file, each shorter prefix is tried as a file, longest first, and the remaining segments descend the loaded table's exported nested tables (a missing segment notessegment '<seg>' is not a nested module table; import binds nothingand the use falls through to the next prefix, Total Grammar: it never rejects). The tail-against-std resolution shortcut is BARE-NAME only:use mylib::seqnever hijacksstd/seq.opby its tail, descent is the only multi-segment fallback. (3) wildcard tail:use mylib::seq::*flat-binds the target table's exports without binding the table itself under a name (an explicitasalias still binds it); later imports overwrite earlier flat binds, first-match-wins at call time. Qualified reads and calls may be spelled with::(mylib::seq::tag()), which is EXACT sugar for the dot form and carries identical AST. - Visibility model (W24).
pubis a CONTEXTUAL marker, not a keyword: at top level,pub gene f(),pub let x = …,pub const c = …, andpub phenotype P { … }mark the name public; an identifier literally namedpub(orpubanywhere else) parses exactly as before. Default mode:pubis INERT, zero behavior change (pinned byte-identically by tests/differential/visibility_default.op, includingpubused as an ordinary variable). Opt-in strict mode,.cell modules.visibility = strict, changes the export rule for that load: a module with pub marks exports ONLY those names (anchor export still wins when present); every other top-level name stays module-private (module-local calls are unaffected, what changes is what the export map carries, so both the qualified read and the flat alias binding see the hidden names as the SOFT tier: null + note, never a crash). The loader notes how many private names were hidden (strict visibility: N private name(s) hidden in '<mod>'). Migration path: a strict module with ZERO pub marks keeps default-open and notes it once, turning strict on never silently empties a legacy module. Both engines byte-identical under the granted cell (tests/granted/visibility_strict.op + .cell in the harness's granted targets). - Resolution algorithm (W069, pinned). The path is tried against candidate roots in order; first regular file wins:
# candidate root class notes 1 the importing file's directory ( interp.base_dir= entry-file dir, tools.rs)doc-relative the CWD never participates here, editors/CI may run from anywhere 2 the path as written, under the CWD CWD plain names resolve here when no doc-relative shadow exists 3 std/under the CWDstd dev-tree layout 4 exe-dir joined with the path (and with exe-parent) exe-relative bundled layout …/bin/operon + …/bin/stdand install/dev layout…/bin/operon + …/std(dx-r5)5 exe-relative std/trees by bare file name (non-std/paths only)exe-relative std lets use xfind the shippedstd/x.op6 $OPERON_STDjoined with the path as written (not double-joined withstd/)override operator override of the managed std tree 7 the vendored dependency cache ( operon.lockname→rev→dir;$OPERON_DEPSor~/.operon/deps/<name>-<rev12>), the leading path segment maps to the pinned checkout:use my-lib/utiltries<cache>/util.opdeps (W19/W23) deterministic: the lock pins name→rev→dir, no globbing; the standard roots above win first, so a local checkout shadows the vendored copy - Failure classes (W070): a miss reports every attempted root for the plain-name class only; the traversal class (absolute/
../~/drive paths) keeps ONE unified "denied or nonexistent" message, attempted-root detail there would resurrect the C-7 filesystem existence oracle (sec-r1). - Parity note (honest scope): the Python oracle implements roots 1–3 + 6; roots 4–5 are runtime-only (the oracle never ships beside a
std/tree). Differential coverage pins roots 1–2 (tests/differential/mod_res.op); root 3 is exercised corpus-wide by everyuse std/…program; roots 4–6 are pinned by the redteam/granted suites where the layouts exist (scripts/install.sh, release artifacts). - LSP divergence (allowed, justified):
operon-lsresolves with the document's directory as root #1 and is deliberately CWD-independent (lsp-r1), editors launch language servers from arbitrary CWDs; CWD-rooted candidates would make diagnostics machine-dependent. Runtime keeps CWD roots becauseoperon run's CWD is part of the operator contract.
- Failure classes (W070): a miss reports every attempted root for the plain-name class only; the traversal class (absolute/
- Import gating (§9b): imports that resolve inside the program's own managed trees, the importing file's project directory, the CWD, the standard library, or a vendored dependency from the lockfile (root 7, the operator resolved + installed it, so it is sanctioned; an arbitrary cache path never enters the mapping), are always allowed; a
usethat reaches outside those trees requires a read capability. - The package system (W19/W20/W23). Package metadata lives in
operon.toml,[package] name/version/operon-version+[deps] <name> = { git = "<url>", rev = "<pin?>" }, a minimal TOML subset, parser-reported line-by-line on anything outside it (a manifest that lies would corrupt the lockfile; the W22 rule keeps.cellruntime-only, the two never merge). CLI:operon mod init(write the manifest) ·add <url> [--rev r] [--as name](clone via the git CLI, pin the resolved rev, vendor under the cache, lock) ·remove <name>·update(re-resolve the closure) ·install(materialize every lock entry, offline whenever the cache is warm) ·tree(the resolved graph) ·verify(recompute every vendored checksum; nonzero exit on tampering). Resolution walks the TRANSITIVE closure (each dep's own operon.toml), cycle-safe, in sorted-name order, the same manifest yields the byte-identicaloperon.lockon every machine. The lockfile (operon.lock) pinsname/git/rev/checksumwhere the checksum is a sha256 content digest of the vendored checkout (in-house FIPS 180-4, zero crates).--lockedfails the run on manifest↔lock drift (missing dep, changed URL, rev mismatch, or a lock entry justified by nothing, direct or transitive). Proof:scripts/pkg_e2e.shbuilds a two-package transitive dependency tree from local repos, locks it, wipes the cache, re-installs FROM THE LOCKFILE ONLY, runs offline, and exercisesverifytamper-detection and--lockeddrift rejection. - A module evaluates to a Map of its exported names. Export rule:
- If the file contains any
anchor export a, b;(at top level or inside a tad), ONLY those names are exported. - With zero
anchor exportanywhere, all top-level genes and lets are exported (default-open).
- If the file contains any
tad Name { ... }, topologically-associating domain: an insulation boundary. Names inside a tad escape ONLY viaanchor exportinside that tad. Tads may not nest (nesting degrades to a merged tad + note). A file may have several tads.anchor import x;, declares an external name the domain expects;operon checkverifies it exists in the file or in used modules (−2 and a finding if not).- Cyclic
use→ second import returns the partial module Map + note (no hang).
A trait is a named set of method contracts. A phenotype can implements any number of
traits (after the optional from Parent in the header); traits and inheritance compose:
trait Show {
gene describe() { return "shape:" + self.display() } # DEFAULT method (has a body)
}
trait Big {
gene area() # REQUIRED method (no body)
}
phenotype Rect implements Show, Big {
let w = 0
let h = 0
gene init(w, h) { self.w = w; self.h = h }
gene area() { return self.w * self.h }
gene display() { return "rect(" + str(self.w) + "x" + str(self.h) + ")" }
}
- Required vs default: a trait method with no body is REQUIRED, the implementing phenotype (or an ancestor) must provide it; a trait method with a body is a DEFAULT, used when the phenotype's own lineage has no method of that name.
- Contract check at construction:
new Rect(...)verifies every REQUIRED method of every implemented trait. A missing one is a rung-4 note,phenotype 'Rect' implements 'Big' but does not provide 'area()', the instance is still built (Total Grammar); the eventual method call wobbles null per §9. Implementing a trait that was never declared notestrait 'X' not declared; contract on 'Y' ignored. - Dispatch order: phenotype's own lineage (child → root) first, then implemented
traits in declaration order (first default body with the name wins), then the
field-as-callable fallback. Dispatch is VIRTUAL: a default body calling
self.display()dispatches back through the phenotype's own methods first, that is the point of theShow.describeexample. - Method semantics are ordinary gene semantics: defaults take params, close over
nothing (they are plain genes with
selfbound), and ride the full call funnel (regulation gates apply, §11). - Redefinition: re-declaring a trait replaces it (last wins, noted), mirroring phenotype redefinition. Trait declarations inside tads contribute their names to the module's member list (§8 export rules unchanged).
- Evidence: tests/traits.op (10 assertions, both cores),
tests/differential/traits.op (byte-identical), fmt round-trips
traitblocks and theimplementsclause canonically.
The error hierarchy (W06, D-014). Operon separates four tiers of "went wrong", each with its own mechanism, everyday failures never touch Stress:
- Null + note, a soft miss (
?.on null, a member that isn't there, a burst-off call). The value isnull, a rung-4 note explains. Expected, routine, unwrappable with??. - Option / Result, an expected failure as a first-class value (below). The caller
decides: propagate with
?!, default withunwrap_or, or inspect withis_ok/is_err. - Stress, an exceptional failure: a contract violation, resource ceiling, or
capability denial. Contained by
stress/rescue. - Hard exit, uncaught stress at the entry boundary renders the traceback and exits 1 (§9a).
Interpolation stresses propagate (no special containment, #213). A stressed
expression inside a string interpolation "{expr}" raises Stress exactly like the same
expression in any other position — there is NO containment inside interpolation and NO
degrade-to-"null" piece. The nearest boundary contains it per the ordinary hierarchy
(an enclosing stress/rescue, the per-statement top-level loop, or hard exit at the
entry). The retired behavior (a rung-4 interpolation stress contained note + the piece
becoming the literal string "null") was an engine-only special case the oracle never
mirrored; worse, it made a stressed while condition truthy forever
(while "{q*w}" { } burned the entire fuel pool, the #213 class-S grind). Clean pieces
are unaffected; ?! inside an interpolation still propagates as the documented return
(the variant reaches the gene boundary untouched).
First-class Option/Result (D-014). Four constructors build variant values: some(v),
none() (the Option family), ok(v), err(e) (the Result family). The tag IS the
contract, families are distinct (some(1) != ok(1)) and type() returns option or
result. Payloads may be any value, including other variants (some(some(9))).
- Predicates:
is_some/is_none/is_ok/is_err(v). - Safe extraction:
unwrap_or(v, default)never stresses, payloads win, everything else (None/Err/plain values) yields the default. - Unsafe extraction:
unwrap(v)returns the payload of Some/Ok; on None/Err (or a plain value) it raises Stress kindunwrap, a programmer-contract violation, i.e. tier 3, deliberately rescue-catchable. Expected failures belong in the value layer (checkis_okfirst, or propagate). - The
try_*family (W06 stage 2): Result-returning variants of the failure-prone core builtins.try_num(s)(parse as int, then float, same rules asnum; the err payload renders the RAW input:Err("num('x9') failed")),try_index(l, i)(Err("index 3 out of range for list of length 3")),try_get(m, k)(Err("no key 'zz'")),try_pop(l)(Err("pop from an empty list")), the latter still mutating. Non-matching argument TYPES areErrpayloads (not stresses); wrong ARITY stays anunfoldedstress (the ok/err rule). Pinned on both engines: tests/differential/try_family.op + tests/try_family.op. -
- Repr:
Ok(3),Err("x"),Some(3),None. Truthiness: Some/Ok are truthy, None/Err falsy (if (result)reads naturally). JSON:{"ok":1},{"err":"x"},{"some":1}; None serializes asnull(in-memory lossless, JSON is a Map-shaped view).
- Repr:
- Equality: same tag + equal payloads;
None == None.
Propagation ?! (D-014). A postfix operator, e?!:
eisSome(v)/Ok(v)→ the expression ISv(payload unwrapped).eisNone/Err(x)→ the enclosing gene returns that variant value, unwinding immediately (statement-level, like areturn). This is a return, not a failure: it crossesstress/rescueboundaries untouched, gains no traceback chain frames, and can never be contained by rescue (not evenrescue any).eis a plain value → silent identity (the?.-on-non-null precedent).- Outside any gene (top level, proof frames, REPL): the signal is contained with a
rung-4 note
propagation reached top level: <value> passes through, Total Grammar never rejects, and the "propagate" kind never leaks as a failure anywhere (the marker rides an interpreter-only payload field no user path can set; theraisegrammar's kind whitelist cannot name it either). - Inside a sequence body: the stream ends cleanly with the note
propagation ended the sequence(sequences are streams, not answers, the variant has no return path). - Repeats compose:
some(some(9))?!?!is9. Binds tighter than every binary/ternary operator; the formatter round-trips it. - Grammatically
?!is one token, lexed before the ternary's bare?, the two never collide.
Compatibility note (stage-2 roadmap, in progress): std functions and builtins that
fail today return null + tier-1 notes or raise tier-3 stresses. They migrate to Result
returns per-function, each documented at migration time; the null/stress contracts stay
the default for 2.x so existing programs keep their byte-identical behavior. First
migrated family (v2.6.0): the try_* core builtins above — additions, not replacements;
the legacy num/index/key-read/pop contracts are pinned UNCHANGED in tests/try_family.op.
Wave 2 (v2.7.0): the extraction, environment and parsing families — try_first(v) /
try_last(v) (legacy first/last: null + note on empty/wrong-type; the Err payload
is the legacy note text minus the "; null" suffix), try_char_at(s, i)
(Err("char_at index 5 out of range for string of length 5"), the try_index shape;
a non-int index is an Err payload, not a stress), try_env(name)
(Err("env 'X' is not set") on the granted lane; a capability denial STAYS an
interference Stress — containment is tier 3, never a value — so zero-grant programs
see the refusal exactly as with env), try_json_parse(s) (legacy raises unfolded
on malformed input; the Err payload is the raw-input echo
Err("json_parse('<raw>') failed")), try_re_groups(pat, s) (legacy bare null on
no-match; Err("no match"), malformed pattern → Err("re_groups('<pat>') failed");
the 2M-step ReDoS ceiling STAYS an overflow Stress — resource ceilings are tier 3).
Engine-neutral payload law: Err payloads are stdout-visible, so they must render
byte-identically on both cores — they are raw-input echoes or fixed strings, never the
underlying engine's parse-error text (the two engines' JSON and regex libraries disagree
on wording). The granted try_env outcomes are pinned in tests/granted/try_env_wave2.op
(explicit cell); the zero-grant refusal is pinned in tests/try_family.op.
Runtime failures raise a Stress value: Map {"kind": Str, "message": Str}. Kinds:
unfolded (type errors), missing (bad index/key/member/null-deref), overflow (int overflow, depth limit, resource ceilings), burned (assertion failures, resource errors), interference (capability-sandbox denials, §9b), unwrap (D-014: unwrap on None/Err or a plain value), any (catch-all position only).
stress { RISKY } rescue (e) { promote("contained: {e.message}") }
stress missing { ... } rescue { ... } # kind filter: only catches `missing`
- Unspecified kind catches any. Filtered kind catches only that kind; others propagate outward.
rescuebinding optional:rescue (e). No rescue clause → stress is contained to Null + note (Top-Grammar runtime law).- Uncaught at top level → printed as containment note, run continues (or ends that entry call with Null).
assert(cond, msg?)raises Stressburnedon failure (this is what proof frames catch).
Every Stress carries a call chain: the gene frames it unwound through, innermost
first. The chain is captured during unwinding at the call funnel (call_gene), each frame
being (gene name, call-site line), the line of the call expression that invoked that
frame. The frame capture costs nothing on the happy path (append-on-error only) and is
capped at 64 frames (note-cap discipline: a bounded chain is a contained chain; deeper
chains keep the 64 innermost frames).
-
Rescue binding surface:
e.chainis a List of Maps{"gene": Str, "line": Int}, innermost frame first, in that field order.e.kind/e.messageunchanged. (The Stress's own origin line is a Rust-side stderr-rendering field and is deliberately not part of the rescue-map contract.) -
Uncaught rendering (exit-1 path, e.g.
--entry): the primary diagnostic rendersfile:lineof the raise (raise statements carry their own line), followed by the chain, one frame per line, thenat main:[contained] [overflow] app.op:2: detonating at depth at boom (app.op:5) at go at mainA frame with line 0 (the runtime-invoked entry gene) renders as a bare
at <gene>. The renderer is also capped at 64 frames (… N more frame(s)). -
Containment: the chain leaks nothing beyond the script path already printed in the primary diagnostic, no environment, no cwd, no host paths (redteam rt_p15a–c).
-
The Python oracle mirrors the capture op-for-op: identical
e.chainvalues (gene names AND call-site lines), differential-pinned intests/differential/traceback_chain.opand shape-pinned intests/traceback_shape.op.
Every UNCAUGHT fatal stress (a stress that escapes the entry-gene call path to the runner) renders as a located block on stderr:
error[<code>]: <message>
--> <file>:<line>
|
<line> | <source line>
|
= <note: the grant clause of a denial, else the kind>
Contract:
- Codes are stable (E1xxx) and derived from (kind, message) by src/diag.rs: E1002 missing, E1003 unfolded, E1004 unwrap, E1010 read, E1011 write, E1012 net, E1013 exit/env, E1014 spawn, E1015 py, E1016 clock, E1019 other-capability, E1020 overflow, E1021 burned, E1000 unclassified. New families append; codes never renumber.
- The W007 call chain renders after the block, innermost frame first, capped
at 64,
at maintail deduplicated against the entry frame. --json-errorsreplaces the block with ONE JSON object on stderr: code, kind, message, file, line, column, length, chain (gene+line array), help (the --allow-* tokens extracted from the message itself). Unknown location fields are null, never guessed.- The caret row appears only for capability denials whose failing call name is found on the raise line (the AST is line-only today; columns arrive with span threading, a dev-1 lane item). No other kind fakes a caret.
- Stresses caught by rescue or soft-contained at the top level keep their existing note format (parity surface, mirrored in the oracle); this section governs the FATAL surface only.
- Pinned by scripts/diag_golden.sh (byte-exact fixtures under tests/diagnostics/); rc stays 1 (dx-r1).
W101 slice 2 extends the same shape to the W041-coded streams (check
correctness findings, lint style findings) in text mode:
warning[W07]: binding 'dead' is never read in this file (dead store); ...
--> <file>:<line>
|
<line> | <source line>
|
= rule: <rule>
- The severity word prefixes the code:
error[E01]:,warning[W07]:,style[N12]:; the fatal block keeps its bareerror[<code>]:header. - Findings carry line-only locations (W042 lane), so no caret row is faked; the note line names the owning rule as provenance. Phantoms are swept without line info and render header-only, code attached (W01).
- Section headers (
error:/warning:/repair:/style ...) and thesummary:line ofoperon checkare unchanged; scripts parse those. JSON surfaces are unchanged. - rc contracts unchanged:
lintstays advisory (0, or 3 under--strict),checkescalates on hard errors (3, dx-r9). - Color (W101 slice 3) is AUTO: ANSI codes appear only when the target
stream is a terminal;
NO_COLOR(no-color.org) always suppresses; and pipes, files and the golden gate receive byte-exact plain text. There is no always-on flag by design: the golden fixtures are the contract, and a flag that leaks codes into them would be a lie generator. The caret row is width-correct on CJK source text (display width, not char count, pads the underline). - W101 slice 5 gives the engine a real span type:
Span(line, char column, char length + the byte offsets that drive width-correct padding) andLabel(a Span, its text, and a primary/secondary kind). The fatal renderer consumes labels through ONE locate site; a secondary label that would overlap an earlier segment on the same line is dropped, never double-marked.--json-errorsgains an additivelabelsarray ({line, column, length, text, primary}per label; empty when no honest location exists). Unknown locations are still null, never guessed; existing fixtures render byte-identically because today's single-caret heuristic is just the one-primary-label case. - W101 slice 6 adds the suggestion layer.
did_you_mean(render-side) mirrors the runtime wobble ladder's thresholds exactly (edit distance 1 for names up to 4 chars, 2 beyond) so the renderer and the interpreter never disagree about what "near" means; output is deterministic (distance, then name) and capped at 3.- Entry validation (rc contract):
--entry gnames a gene the program must define. An undefined entry gene is a FATAL (rc 1, dx-r1):error[E1002]: entry gene 'g' is not defined in this programwith a did-you-mean help line; the old behavior (wobble-repair to the nearest builtin, run nothing, exit 0) was a silent no-op disaster and is retired.--json-errorscarriessuggestions(additive array). - Phantom calls grow evidence: check's phantom sweep records the
call-site line (Expr::Call stamps it), the did-you-mean shortlist
against defined genes + module exports + builtins, and a
machine-applicable
SuggestedFix(line, char column, length, replacement, note) when the token is locatable on its line. Word-boundary locating:mininsideadminnever underlines. The check --jsonphantomsitems are objects now ({name, line, suggestions, fix}— additive;fixis null when none). Text output renders the located block with a caret under the call token, the suggestion in the header, and ahelp: machine-applicable fix: ...line; a phantom with no honest line stays header-only. - Help sections order: name-level help (did-you-mean) first, then
grant-level help (
--allow-*tokens), then the W007 chain.
- Entry validation (rc contract):
The runtime is default-deny: a program is an organism in a culture flask, and nothing outside the flask exists until the operator grants it. The builtins read_file, write_file, append_file, exists, read_dir, file_size, fs_delete, fs_rename, fs_mkdir, run, py, http_get, serve, env, and exit raise catchable Stress interference when no grant covers the access, RNA-interference: the cell's antiviral machinery silences the operation instead of crashing. recv_request/send_response poll a queue that only serve fills, so they are inert without a granted server.
Grants (operator-side, CLI):
operon run app.op --allow-read /data --allow-write /tmp/out \
--allow-run gzip --allow-net 127.0.0.1:8080 --allow-env API_KEY
operon run app.op --allow-py math --allow-py numpy # substrate-r1: per-module Python bridge grants
operon run app.op --allow-all # open flask (for scripts that mean it)
-
Path grants (
read/write) resolve symlinks: the requested path is canonicalized before comparison against the canonicalized grant. A grant that normalizes to the empty string (/, or.from/) would match everything and is rejected at startup. -
sec-r5 (anti-TOCTOU): file I/O verifies the opened handle, not the path. The old check → canonicalize → write sequence was raceable with a symlink swap (proven live: a flipped link landed attacker-controlled bytes outside the grant). Now
read_file/write_file/append_fileopen the file FIRST, verify the handle's true identity (Unix: the fully-resolved/proc/self/fd/<n>target; elsewhere: symlink/reparse handles are refused), and do all I/O through the handle, there is no later path re-resolution to race. A dangling outside-pointing symlink is resolved (chains up to 40 hops) and rejected before open, so it cannot be created-through as an empty file. Residual (documented): a race that flips a link to a dangling outside target inside the check-to-open window can create an empty file outside the grant, no content ever crosses. -
sec-r5:
read_filereads regular files only. FIFOs (which would blockopen()forever) and device files like/dev/zero(an infinite byte well that OOM-killed the host) are refused with Stressinterferencebefore open; the stat'd size is charged against the aggregate allocation ceiling before the read. -
--allow-nettakeshost:port;--allow-envtakes a variable name;--allow-runtakes a program name;--allow-pytakes a Python module name (substrate-r1: grant granularity is per module,--allow-py mathrunsmath.*calls and nothing else; granting a module is an explicit trust act, the module executes with the interpreter's OS privileges). -
substrate-r1, the
pybridge (D-010):py(module, "dotted.func", args?)calls into the Python ecosystem through an isolated-mode child (python3 -X utf8 -I -B) running an embedded one-line-JSON runner. The child gets the same scrubbed environment asrun()children; the request is one JSON line on stdin (then the pipe closes); the response is the LAST non-empty stdout line, so modules that print during import or call cannot corrupt the protocol. Result is a map{ok, value, error, code}: Python exceptions are DATA (ok:false,code:1, the exception line inerror), protocol failures arecode:-2, a timeout kill iscode:-1with a note, the interpreter never crashes on a misbehaving module. Container ceilings mirrorrun(): wall time is fuel (1000 steps/ms),.cell py.timeout_ms(default 10000, clamped 1..300000), output capped at 64 MiB per stream. Marshal rules: Operon values ride the language's own JSON serializer (Seq → base string); Python side converts numpy scalars via.item(), ndarrays via.tolist(), tuples/sets to lists, bytes via UTF-8 decode, dates/Decimals to ISO/float; non-finite floats are refused by the runner (ok:false). The bridge is how Operon joins the scientific-Python wall (§1) instead of fighting it: the stdlib stays small because NumPy/SciPy/Biopython are one granted call away. -
The bridge is OPTIONAL (W078). Operon is fully functional with zero Python present,
py()is the only surface that touches it, and the language, stdlib, toolchain, and every non-pybuiltin are unaffected by its absence. Default state is OFF: no grant →py()raises catchable Stressinterference(capability fence, §9b) before any interpreter is probed. With a grant but no interpreter on PATH, the bridge reports a clean operational error, Stressmissing, messagepython interpreter not found (tried python3, python), never a crash, never a hang. A Python-absent machine must pass the entire non-pytest corpus byte-identically (the differential harness itself runs the Python oracle, which is a TOOLCHAIN dependency, not a runtime one, the shippedoperonbinary needs no Python to run, check, fmt, build, doc, or watch). -
Python version contract (W079, §15b).
Question Contract Supported floor Python ≥ 3.10. The bridge child reports its version ( pykey in the protocol response); a run whose interpreter is below 3.10 emits a rung-4 note once per run:py bridge: interpreter X.Y is below the documented support floor (3.10); behavior may drift, the call itself still proceeds.Newer interpreters Supported forward-as-is: 3.11/3.12/3.13+ are expected to work; any break is a bridge bug to file, not a new support tier. Third-party packages No guarantee is made for NumPy/pandas/SciPy beyond what the marshal rules below state (numpy scalars/ndarrays convert via .item()/.tolist()); if a package exists in the environment it is importable, full stop. Package absence isok:falsedata, not a crash.Startup cost Each py()call spawns a fresh isolated-mode child (isolation over speed, deliberately); wall time is charged as fuel. Callers batching many calls should amortize at the Python side (onepy("module", "batch_driver", [args])per workload, not one per element).Windows caveat python3is probed first, thenpython(the Windows launcher name); if neither is on PATH the bridge reports the same cleanmissingStress as any other absent interpreter. -
.cellgrant keys (allow.read = /data, …) are honored only when the config is loaded explicitly via--cell file.cell. An auto-detectedoperon.cellcannot grant capabilities, itsallow.*keys are ignored with an[info]note (a file that happens to sit in the project must not silently widen the sandbox). -
useimports: files inside the program's own project directory, the CWD, or the standard library are always importable (otherwise nothing imports under default-deny). Ausethat resolves outside those managed trees requires a read grant; the denial is Stressinterference.
Resource ceilings (all raise catchable Stress):
| Resource | Ceiling |
|---|---|
| Recursion depth | 10,000 (overflow) |
| Step budget | 500,000,000 steps per RUN — one pool shared by the host and every worker cell (§17 wave-3 amendment 4; spawn/sequences no longer mint fresh budgets) (overflow "step budget exhausted"); --fuel N lowers it |
String .repeat() allocation |
512 MiB |
| Aggregate run allocation | 2 GiB (push/concat/repeat/interp assembly all charged; sec-r5: large-string variable reads and writes/reads of file bytes are charged too, a 500 MB string read three times IS 1.5 GB of real allocation) |
Collected run() child output |
64 MiB per stream (collected prefix is returned; the child is still timeout-killed) |
Collected py() response (substrate-r1) |
64 MiB per stream, same contract as run(); wall time charged as fuel |
| Parse/lex notes | 10,000 per parse (further notes suppressed) |
distance() dynamic-programming table |
10,000,000 cells |
sleep() |
60,000 ms (sleep escapes the step budget, so it is capped) |
json_parse nesting |
512 levels |
| Integer arithmetic | i64, overflow → overflow (no wrap) |
| Map non-scalar-key scan (sec-r5) | 512 entries per LOOKUP, beyond that a non-scalar (list/map) key is treated as absent. Container keys never upsert (the always-append coexistence law, mirroring the Rust MapStore KC_OTHER arm op-for-op): an insert with an equal-content container key ALWAYS APPENDS a new pair — equal-content keys coexist, lookups and del act on the FIRST match. Scalar keys keep exact semantics via the hash memo |
The W32 overflow contract, stated once and pinned everywhere. There is no silent wrap on any integer surface: +, -, *, **, //, %, unary negation, and abs raise catchable overflow at the exact i64 boundary with per-op messages (int overflow in '+' etc.); shifts require the amount to be in 0..=63; // and % guard the i64::MIN / -1 corner on both engines (the oracle materializes the same corner stress the Rust core raises); / by zero is unfolded, never overflow; float * and / reach inf/nan as ordinary VALUES, while float ** overflow raises; mixed int/float arithmetic widens and never overflows. The whole contract is pinned with exact kind+message asserts in tests/overflow_contract.op (both engines) and byte-identical output in tests/differential/overflow_contract.op. BigInt / Decimal: a native BigInt/Decimal type is deliberately deferred until a real use-case forces it (a crypto std would); the sanctioned escape hatch is std/bigint, exact arbitrary-precision integers over sign-magnitude base-10^4 digit lists (big_from_int, big_from_str, big_to_str, big_to_int, big_cmp, big_add, big_sub, big_mul, big_pow, big_fact, big_neg, big_abs, big_is_zero), pure .op, deterministic, byte-identical on both engines. big_to_int returns null (never a clamp) when the value does not fit i64; parse functions return null on malformed input; zero is always canonical (-0 normalizes, so structural == works).
sec-r5 containment semantics worth stating plainly:
- DAG-shaped values serialize and compare in linear time.
json_str, structural display (print), anddeep_eq(==) keep their visited/comparison sets for the whole walk instead of unwinding them. A shared (aliased) subtree is rendered once; later references render asnull(JSON) or the[...]/{...}marker (display), the same containment CPython applies to cycles, extended to aliased DAGs, which previously re-walked exponentially (l = [l, l]chains: 2^45 node visits for one builtin call). json_stremits valid JSON only (RFC 8259). Non-finite floats serialize asnullinstead of bareinf/nantokens that no JSON parser accepts.- The aggregate allocation ceiling and the step budget are monotonic. Once crossed, the counter stays over the limit, every later charge fails, including charges inside a
rescuehandler, so a run that breached a resource ceiling ends with the top-level containment note rather than resuming. Resource-exhaustion overflow is the one stress a rescue cannot recover from; the contract is a bounded note, never an OOM kill or allocator abort. - CI/supply chain: every GitHub Action in both workflows is pinned to a commit SHA (checkout, toolchain, gh-release, D-6 closed);
Cargo.lockhas zero runtime dependencies (build-time only: cc, shlex, find-msvc-tools).
Biology-facing builtins (
transcribe,translate,reverse_complement,gc_content,find_orf,codon,distance/similar): the contract is here; the mechanism honesty grades live in the modeling track (docs/spec/BIO-CONTRACT.md§2,docs/spec/MODELING-NOTES.md).
Builtins — core: promote(*a) (print, space-joined, returns null) · len · push(l,v) · pop(l) · insert(l,i,v) · remove(l,i) · keys(m) · values(m) · has(m,k) · del(m,k) · range(a, b?, step?) (returns List) · str · num (fails → 0 + note) · type (null bool int float str list map gene native sequence, or the phenotype name for instances) · abs min max sum · floor(x) ceil(x) (→ Int) · sqrt(x) (→ Float; a NEGATIVE argument raises catchable unfolded "sqrt of negative number" on both engines — pinned by tests/l1a_num_builtins.op + tests/differential/math_domain.op) pow(b, e) (→ Float) · clock() (monotonic seconds, float) · now() (monotonic seconds, float — the same high-resolution timer under a briefer name) · exit(n?) (capability-gated, sec-r2: kills the host process, so it is default-deny — grant with --allow-exit or .cell allow.exit = true) · assert(c, msg?) · codon(s) (0–100 style score of an identifier) · distance(a,b) (edit distance over CHARACTERS — the Python-str law the oracle defines (Z-119: "héllo" vs "hello" is 1, not the byte count 2); 10M-cell ceiling measured in chars and a 64 KiB per-operand cap — over-budget pairs never win a nearest-match contest; the C++ byte kernel remains the parser-wobble/diagnostics engine, where the ASCII keyword domain makes the two laws coincide) · similar(a,b,maxd?) (bool) · transcribe(dna) · translate(rna) (stops at stop codon) · reverse_complement(dna) · gc_content(dna) (0–100) · find_orf(dna) (list of ORF proteins) · memory() (W097 accounting — map arena_bytes, interns, allocs from the in-process symbol table: arena_bytes = live bytes held by the Rust-owned intern table (symbol spellings; NOT the process RSS, NOT interpreter values — refcounted values live in ordinary Rust allocation that this counter does not see); interns = count of interned canonical spellings (one per distinct identifier the process has lexed, oldest-first in :symbols); allocs = allocation operations served by the table since process start (monotonic, never reset). All three are process-wide, cross-run cumulative for one binary invocation, and thread-safe (mutex-guarded). Honest limit: this is a symbol-table gauge, not a heap profiler — per-value/per-gene accounting is the MEM-PROFILER design (docs/design/MEM-PROFILER.md, deferred W097-v2); cycles (W013/D-013) = live detected reference cycles, a fourth process-cumulative key with its own contract (registration at the edge that closes a cycle, prune only on PROVEN death or breakage, bounded deterministic walks, §19e) and the weak-handle API around it (§19f)) · weak(v) / strengthen(w) (W013 weak references: non-owning handles to list/map/phenotype values, dead derefs answer null, refusals and membrane rules in §19f) · methyl(key, default?).
Builtins, randomness and dynamic dispatch: random() (float in [0,1)) · random(n) (int in [0,n)) · randomize(seed?) (deterministic xorshift state, identical in both implementations) · chr(i) · ord(c) · argv() (List of the arguments after the script path) · sleep(ms) (≤ 60,000 ms) · call(name_or_gene, args_list) (dynamic dispatch, resolves builtins, named genes, or gene values).
Builtins, iteration and numeric (v2.3): enumerate(x) (List of [i, v] pairs; List or Str) · zip(a, b) (List of [va, vb] pairs, min length) · sorted(l, cmp?) (new list; same comparator contract as .sort, cmp true when a belongs before b, or the default mixed-type order; the comparator sort is STABLE — ties keep input order on every surface, #137) · reversed(l) (new list) · any(l) / all(l) (truthiness; empty → false / true) · first(l) / last(l) (element or char; empty → Null + note) · take(l, n) / drop(l, n) (clamped slices; List or Str) · unique(l) (deep-equality dedup, first occurrence kept) · flatten(l) (one level) · chunk(l, n) (size-n groups, last partial; n ≤ 0 → note) · round(x, d?) (d omitted/0 → Int, else Float; half away from zero at the d-th decimal, computed in the same f64 formula in both implementations, round(1.005, 2) == 1.0 because 1.005 is stored as 1.00499…) · clamp(v, lo, hi) (numbers; non-number → Null + note) · divmod(a, b) ([q, r] with the exact // and % semantics, floored, sign follows divisor). Wrong-type inputs return Null (or false for any/all) with a note, never a crash.
Safe accessors (v2.3): m.get(k, d?) (Map; deep-equality key lookup; missing → default when given, else Null + note) · l.get(i, d?) (List; negative index from the end; out of range → default or Null + note) · s.at(i, d?) (Str char; same contract). The bare forms (m.k, l[i]) keep their existing behavior, .get/.at add an explicit default, they do not change any existing read.
Builtins, JSON: json_parse(s) (→ value; nesting > 512 → Stress) · json_str(v).
Builtins, capability-gated (§9b; denied → Stress interference): read_file(p) (regular files only, FIFOs/devices are refused interference, sec-r5) · write_file(p, s) · append_file(p, s) · exists(p) · read_dir(p) (List of names) · fs_delete(p) (files and EMPTY dirs; symlinks are refused outright, dx-r6) · fs_rename(from, to) (both paths need a write grant) · fs_mkdir(p) (create_dir_all semantics) · run(prog, args?) (map code stdout stderr ok; the child runs under a wall-clock timeout, .cell run.timeout_ms, default 10 s, clamped 1..300 000, and the child's wall time is charged as fuel exactly like sleep; output may be truncated when a grandchild holds the pipe past the 250 ms drain grace) · py(module, "dotted.func", args?) (substrate-r1 Python bridge; map ok value error code; per-module grant --allow-py m; isolated-mode child, timeout .cell py.timeout_ms, wall time is fuel, 64 MiB output cap; Python exceptions are data ok:false, never crashes, full contract in §9b) · http_get(host, port?, path?) (response body) · serve(port?) · recv_request() (map conn method path body, or null) · send_response(conn, status?, ctype?, body?) (status/ctype containing control characters are refused, no header injection) · env(name).
Builtins, concurrency, telemetry, regulation: fingerprint() (run telemetry, §14) · spawn(f, args?) → id · join(id) → value (default wait ceiling 300 s, timed-out tasks stay joinable, join returns null; explicit join(id, ms) unchanged) · cancel(id) / task_state(id) / cancelled() (W18 cooperative cancellation, full contract §13) · wait_all(ids) / wait_any(ids, ms?) (W15 task groups, §13) · channel() / send(ch, v) / recv(ch) / close(ch) / select(ch...) (W015 channels, full contract §13) · toggle_on(name) · toggle_state() · repressi_next() · repressi_state() (fuel-charged: integrating new ring ticks costs 20 steps/tick) · repressi_start(ms) (one shared cancellable timer per run, restarting retires the old thread) · grn_fire(name, decay?) · grn_state() · grn_set(name, v) (write a node's level, clamped 0..1) · grn_get(name) (read a node's level) · methylate(name) / demethylate(name) (runtime methylation, same graded semantics as the @methylate/@acetylate marks: level +1 / saturating −1, gate applies at the next call; returns the gene's new level; keys are allocation-charged like any growth) · m6a_write(name, n?) / m6a_erase(name, n?) (reg-bio-3: quantitative m6A site density 0..=3, writer/eraser dose, default 1; resistance to redefinition requires level ≥ 1) · passage(n) (reg-bio-3: n cell divisions, methylation levels dilute by .cell methyl.maintenance with half-down rounding, generation advances; returns the generation) · expr_on(kon?, koff?) / expr_off() (reg-bio: the telegraph promoter layer, §11; in-source switch for stochastic expression, seeded via randomize).
Str methods: .upper() .lower() .trim() .split(sep) .join(list) .replace(a,b) .contains(x) .starts(x) .ends(x) .repeat(n) .slice(a,b) .len()
List methods: .map(f) .filter(f) .reduce(f, init) .each(f) .sort(cmp?) .reverse() .contains(x) .index_of(x) .slice(a,b) .join(sep) .len() (cmp returns true when a before b; comparator sorts are STABLE — elements the comparator orders equally keep their input order, so multi-key sorts compose; #137 / Z-137-SORTSTABLE, pinned by tests/differential/sort_stability_pin.op; .sort(cmp) is the immutable-method contract: it returns a NEW sorted list and never mutates the receiver)
Map methods: .keys() .values() .items() .has(k) .del(k) .len()
Bio-layer note (§11a): the codon-kernel family and sequence utilities in the core row above (codon distance similar transcribe translate reverse_complement gc_content find_orf) and the bio half of the regulation row (toggle_* repressi_* grn_* methylate demethylate m6a_write m6a_erase passage expr_*) are the biology layer's runtime surface; the mechanisms live in §11 and the crossings are stated once in §11a.
Track split (W091): this section is the LANGUAGE contract — syntax, exact semantics,
gates and their order, clamps, knobs, entropy discipline, and the tests that pin them.
Biology rationale, term audits, and not-modeled lists live in the MODELING track:
docs/spec/MODELING-NOTES.md (markers [MN-*] resolve there; the same document carries
the §16 biology ↔ feature map). Editing this section is a language change; editing the
modeling track is a docs change — see CONTRIBUTING §8b.
-
tad/anchor— §8. Module insulation with export anchors. -
enhance a, b, c;— super-enhancer cluster: marks genes with an activation boost (v2.2). Under the GRN call gate, an enhanced gene lowers every incoming activating threshold by 0.25 (floored at 0; the dose is tunable via.cell enhance.delta; modeling note [MN-enhance]) — an enhanced gene fires where an unenhanced one stays gated (e.g. regulator level 0.4: threshold 0.5 blocks the plain gene, passes the enhanced one). Genes without the mark are unaffected and edges without a threshold stay declarative, so old programs keep running (§5 default: the boost exists only whereenhancewas declared).operon profileshows theenhancedflag;operon checkgives the file a codon-score bonus for enhanced hot genes; the NMD untranslated sweep skips enhanced genes. -
@acetylate— histone acetylation mark: excluded from silence rewriting and immune to the silencing gates (active chromatin stays active — open chromatin wins, D-005); shown asactivein profile. (Term audit, reg-bio: [MN-@acetylate].) -
@methylate— histone methylation mark: gene is repressed, graded (v2.2). Every executed@methylate-markedgenedefinition deepens that gene's silencing level by 1; an@acetylate-marked definition relaxes it by 1 (competing marks: [MN-@methylate]). A call to a gene whose level has reached the threshold (default 3, tunable via.cellmethylate.threshold = n) is blocked: it returnsnullwith a fallback note ("methylation silences: … — call returns null"), never reaches the gene body, and does not count infingerprint()/burst telemetry — repressed means repressed. Below the threshold, calls execute and the first call emits the soft note "methylated call" (later calls are silent — the cell does not narrate every repression; suppressed entirely when.cellsetsmethylate.quiet = true). Marked genes are exempt from the NMD untranslated sweep, and@acetylategenes are exempt from the methylation gate itself (open chromatin wins). One mark (the common case) never silences — old programs keep running. -
@m6a— m6A mark = dispatch priority + redefinition resistance (term audit, reg-r4: [MN-@m6a]): among same-name candidates (splice variants, shadowing), the m6a-marked one wins resolution (variant selection order:.cell> CLI > m6a-marked > first declared — a marked variant beats first-declared), and an m6a-marked binding resists redefinition — an unmarked re-geneof the same name is ignored with a note ("@m6a-stabilized; redefinition ignored"); mark the new copy too, to replace it. -
silence old -> new strength s sites n;/silence old;— RISC-style silencing, now STOICHIOMETRIC (reg-bio-3, C9). The redirect form: every call to geneoldis redirected tonewwith a note "RISC: call silenced". The target-less form (reg-bio) is pure degradation [MN-silence]: every call tooldreturns null with a "RISC degraded" note, and a degraded call is not expression (it never reaches the call counters or burst telemetry). Dose (reg-bio-3):strength sis the per-site capture probability (clamped 0..=1, default 1.0) and eachsilencestatement for the same target is one binding site (sites ncomposes multiplicatively, 1..=64): the per-call capture probability is1 − Π(1−sᵢ)^sitesᵢ. A sub-1.0 capture draws ONCE per call attempt on the shared mirrored xorshift64* stream (the telegraph-promoter discipline; the oracle mirrors it op-for-op); a captured call takes the redirect/degrade path, an escaped call proceeds through the pinned funnel with a one-time "RISC escape" note.strength 1.0with one site is the legacy binary redirect and consumes NO entropy — therandom()stream is untouched for every legacy program.@acetylategenes are immune, and the immune check precedes any draw (immunity costs no randomness). Same-name splice variants resolve first, then silencing applies to the resolved name. -
NMD sweep (
operon check --nmd): "premature stops" = unreachable statements after an unconditionalreturn(reported −4 each); "untranslated transcripts" = defined, never-called, non-exported, non-enhanced genes (info, −1).--nmd purgerewrites the file without them. -
splice name { variant a { } variant b { } }— alternative splicing:name(...)dispatches to the active variant. Selection order:.cellvariant.name=v> CLI--variant v> m6a-marked variant (v2.2) > first declared. Variant declarations may carry histone marks (v2.2):@m6a variant v { }makesvthe m6a-priority variant;@acetylate/@methylateon a variant ride on the resolved binding (acetylate immunity applies when a marked variant is active). Marks stack and can precede the variant keyword in any order; a mark not followed byvariantis noted and skipped. Variants take parameters.operon build --variant abakes one variant into a standalone file (build caveat: a variant's parameter list is not yet carried into the baked file — bake param-less variants, or keep the splice in the source). -
.rnaedit patches —edit target { replace "src" -> "dst"; }where target is a file name or gene name; applied to that target's source text before parsing. CLI:operon run app.op --rna hot.rna. Each applied replacement emits a note. Node-addressed v2 (W067): a patch whose first content line issyntax: v2dispatches to the AST editor instead —rename <path> -> <new>,delete <path>,body gene NAME[#N] { ... }over pathsgene NAME[#N]/splice ROOT/variant ROOT.NAME/phenotype NAME/method PHENO.GENE/fate NAME/regulate #N; edits apply to a freshly parsed AST and the result is reprinted through the canonical formatter (apply output is fmt-stable by construction). All-or-nothing: any missed/ambiguous target refuses the whole patch (exit 1, per-rule fate report, nothing written); bare names shared by 2+ declarations require an ordinal (gene foo#2). The reprint drops plain#comments, so v2 refuses such sources unless--allow-comment-drop. CLI:operon rna app.op patch.rna [--write] [--json] [--allow-comment-drop]; header-less patches keep the v1 text semantics byte-for-byte. v1 deprecation (W067 stage 3, W63 step 1 — info since 2.2.0): on theoperon rnaeditor path a header-less patch emits an info note on stderr and--jsonreports"engine":"v1","deprecated":true; the patched FILE output is unchanged. Severity escalates per W63 (warning, then removal one minor later); therun --rnapre-parse path is deliberately note-free (differential stderr parity). Design + as-built:docs/design/RNA-V2.md. -
.cellmethylation config —key = valuelines,[section]headers,#comments. CLI--cell f.cell; elseoperon.cellauto-detected. -
.cellis RUNTIME configuration ONLY (W22, audit item 22) — the hard boundary:.cellkeys configure the interpreter for one run (behavior knobs, capability grants, feature flags). Package/project metadata —name,version,operon-version,dependencies/deps,authors,license— belongs in theoperon.tomlmanifest (W19/W022, track L3c), never in.cell. The two files never merge: a package installed by a registry must be able to ship its own runtime config while the parent project's manifest stays authoritative. Thecell/package-keyslint (W48) will warn when package-like keys appear in a.cell; until then, unknown keys are inert data readable viamethyl()(the W66 formal schema, dev-3, defines the full key inventory). Read viamethyl("k", d?). Impl-consumed keys:variant.<splice>(active variant),methylate.quiet(suppress the methylated-call note),methylate.threshold(graded-silencing gate, default 3),grn.decay(default GRN pulse decay, reg-r2),run.timeout_ms(child-process wall-clock timeout forrun(), default 10000, clamped 1..300000),py.timeout_ms(Python-bridge wall-clock timeout forpy(), default 10000, clamped 1..300000, substrate-r1),wobble.strict = true(a run with any rung ≥ 3 note exits 3, same as--strict),entry(cap-independent default entry, below CLI--entryand abovemain), andallow.*capability grants — honored only with explicit--cell(§9b;allow.exit = truegrants the exit capability, sec-r2),operon.polarity(loop-9 D1 expected-value polarity weight, default 0.5), and the loop-10 Rho/queue knobs:rho.termination(the opt-in layer switch, default off),rho.catch(per-cistron catch probability, clamp 0..1, default 0.5),rho.queue_floor(shield threshold, default 0.5),ribosome.queue_cap(queue saturation, default 1.0),ribosome.drain(per-integration drain, default 0.5). -
ires name;— internal ribosome entry site: declares a cap-independent entry gene.operon run --iresoverrides the canonicalmainentry and runs the first declaredirestarget; a file with nomainuses it automatically. Entry precedence: CLI--entry>.cellentry>main> firstires. -
fate Name { state a -> b, c; state b -> a; enter a; }— fate landscape state machine.let m = Name()enters theenterstate (default: first declared). Methods on instances:.shift(s)(true if transition allowed; invalid → note + false, state unchanged — the valley stay),.state()(current),.can(s)(bool). Instances are Maps with hidden#fate/#statekeys. -
regulate { a activates b strength 0.8; c inhibits d; }— gene regulatory network (default strength 1.0). The network is stateful: by default levels persist acrossgrn_firecalls (a latch — the honest default; real dilution needs the decay below), and each fire seeds additively, capped at 1.0. Decay (reg-r2): opt-in dilution — before every pulse, every existing level decays by a configurable fraction:grn_fire(name, f)takes the pulse's decay directly (0..1, clamped), or.cell[grn] decay = fconfigures it globally; unset/0 is byte-identical to the pre-decay behavior. (Term audit, reg-r3: [MN-regulate].) Each fire runs in two phases: (1) activation propagates in waves,child = max(child, parent × strength^wave)— hop attenuation over up to 10 waves; (2) inhibition is applied exactly once per fire,child = max(0, child − parent × strength), based on the source's post-activation level — a repressor's concentration sets the output level, it does not compound across waves. An edge may carry an optionalthreshold t(dose-response): influence =strength × pⁿ/(pⁿ + tⁿ)where p is the parent level — weak below the threshold, saturating above it. The exponentnis the Hill coefficient (cooperative binding, reg-bio):hill n(integer 1..=8; canonical edge orderstrength → threshold → hill → any) overrides the default n = 2 per edge —hill 1is the graded Michaelis shape, higher n is ultrasensitive, and the exponent is computed by repeated multiplication (neverpow) so both implementations stay bit-identical. An edge may also carry the keywordany: it becomes an OR member — an alternative activator that alone suffices.hill/anyon an edge without a threshold, oranyon an inhibitor, are noted and ignored (Total Grammar).grn_state()returns the level Map (all genes in the graph);grn_set/grn_getwrite/read single node levels (clamped 0..1) so hosts can steer gate state without re-firing. Levels gate calls (v2.2): a gene with an incoming activating edge carrying an explicitthreshold texecutes only whilelevel(source) ≥ t; several activating edges form cis-regulatory input functions (reg-bio) — every AND member must pass, and anany(OR) member alone suffices: the gate opens iff (every AND member passes) OR (any OR member passes); an all-OR promoter with every member below its threshold vetoes with "no OR activator above threshold". An incoming inhibiting edge with an explicit threshold silences the callee whilelevel(inhibitor) ≥ t— inhibitors veto independently of the OR group. A gated call returnsnullwith a fallback note ("grn gate: … call suppressed"). Edges without a threshold remain declarative (they shapegrn_firelevel dynamics only), and a threshold of0never blocks: declaring a network changes no call behavior unless a program opts in with explicit thresholds. The gate is checked at the gene-call funnel (named calls, higher-order calls, and RISC-redirected calls all pass through it) and precedes the call counters — a suppressed call is not expression and does not count infingerprint()/burst telemetry. -
toggle a, b;— a mutual-repression latch: a declared boolean invariant, not rate-based bistability (no cooperativity, no hysteresis; term audit reg-r3: [MN-toggle]).toggle_on("a")turnsaon andboff;toggle_state()→ Map of both. Exactly one may be on — and the pair gates calls: calling the repressed allele returnsnullwith a note ("toggle repressed: … is the inactive allele");@acetylategenes are immune (open chromatin wins). Rate-based bistability IS expressible on theregulatelayer: two mutual inhibiting edges withhill ≥ 2land the losing allele in a partial, cooperativity-dependent band (tests/grn_bistability.op proves the cooperativity-dependent partial bands; HYSTERESIS is not yet demonstrated by a sweep test — loop-8;std/motifsships the circuit). -
repressilator a -> b -> c;— repressive oscillation ring with emergent mutual repression (reg-r2): the ring integrates a reduced, protein-only discrete variant inspired by the Elowitz–Leibler repressilator (term audit, reg-r3: [MN-repressilator]). Each substep, node level evolves asdA/dt = α / (1 + Rʰ) + basal − γAwhere R is the level of the repressor (ringa -> b -> cmeans a represses b, b represses c, c represses a), Euler-integrated with 20 substeps ofdt = 0.05per ring tick (α = 10, γ = 1, init[5, 0, 0]). The oscillation — a ~6-tick limit cycle with one-tick production lag between a repressor's peak and its target's trough — emerges from the loop itself; nothing is scheduled, so gates can read the ring: a GRN edge sourced by a ring node reads the node's normalized level (raw/α, clamped 0..1) at the current tick.repressi_state()returns the level Map and is a pure function of the tick count — manual mode (repressi_next()advances one ring tick) and wall-clock mode (repressilator … period s;orrepressi_start(ms)) fold the identical arithmetic and can never diverge; the Python oracle mirrors it op-for-op bit-identically (§17). Kinetics are load-bearing (reg-bio, F-5): inlinerepressilator a -> b -> c alpha 20 basal 0.5 noise 0.1 seed 42;(canonical order alpha, gamma, hill, basal, noise, seed) or.cellkeysrepressi.alpha/gamma/hill/basal/noise/seedoverride the historical constants per-field (the last declaration wins per-field;.cellapplies first, declarations layer on top).basalis the promoter-leak term [MN-repressilator];noiseinjects a per-substep kick — reg-bio-2 (12-d D6): the kick is MULTIPLICATIVE and dt-aware,v *= 1 + noise·(1/√20)·(2u−1)(rationale: [MN-repressilator]) — drawn from a stream derived from the ABSOLUTE (tick, substep) position — so the fold-from-init path and the incremental cache path stay bit-identical and the oracle mirrors it op-for-op — and it never perturbs the program'srandom()stream. Defaults (α=10, γ=1, h=4, basal=0, noise=0) are byte-identical to the pre-parameterized ring. A secondrepressilatordeclaration replaces the ring. The ring ridesspawnsnapshots frozen at the spawn tick (§13). -
Telegraph promoter layer (reg-bio, F-1) — stochastic expression [MN-telegraph]. Off by default (the deterministic contract is untouched for every existing program).
expr_on(kon?, koff?)— or.cell[expression] stochastic = truewithkon/koff/seed— turns it on: each gene carries a promoter state (initially active) and every call attempt draws once on the shared mirrored xorshift64* stream — an active promoter switches off with probabilitykoff, an inactive one switches on with probabilitykon. A burst-off call returnsnullwith a "promoter inactive" note, never reaches the call counters, and is recorded infingerprint().bursts. The state persists across calls — that persistence IS the burst.randomize(seed)fixes the stream (reproducible bursting, identical in both implementations);expr_off()restores determinism mid-run. Bursting is universal (open chromatin bursts too): no@acetylateexemption. Worker cells inherit promoter states and burst counters in the regulation snapshot (§13). Per-gene promoter identity (loop-9, F-2):@burst kon koffmarks a gene with its OWN telegraph rates (clamped 0..=1), overriding the global parameters for that gene alone — different promoters have different (kon, koff); that is their identity. The mark rides the gene definition (workers inherit via the Arc); draw count and stream order are unchanged. Attempt telemetry (loop-9, F-3):promoter_telemetry(gene)returns{attempts, on_total, episodes, on_frac, burst_size}— every call that reaches the promoter gate is an attempt; episodes are maximal ON-runs;burst_size = on_total/episodesis the mean ON-run length, ~1 under Poisson-like firing and >>1 under bursting. Telemetry rides the snapshot so worker cells never desync from the host mid-burst. Boundary honesty: episodes count COMPLETED ON-runs, so a run ending mid-ON slightly overestimates burst_size (the trailing episode is uncounted) — finite-sample bias, deterministic, documented here rather than hidden. tests/burst_identity.op pins the identity separation on both cores; tests/burst_modulate.op pins runtime modulation AND the@burst > burst_setprecedence. Runtime modulation (loop-9, R9 jury):burst_set(gene, kon, koff)retunes ONE promoter's switching rates at runtime — regulators change promoter switching, not just transcript gating;burst_set(gene)clears the override. Precedence:@burstmark >burst_setoverride > global rates; rides the snapshot (workers freeze modulation at spawn). tests/burst_modulate.op. -
a translates p rate r decay d;(reg-bio-2, C1) — the translation layer [MN-translates]. Atranslatesedge (insideregulate, verbtranslates; optionalratedefault 1.0,decaydefault 0.0) makes the target protein node integrate one Euler step of the classic two-tier ODE at every engine update point (eachgrn_firepulse and each decay-clock tick):p += rate·Δcalls − decay·p, clamped 0..1, where Δcalls is the source gene's call-count delta since the last integration (checkpoints start at 0). Protein nodes live in the GRN level map, so any gate can be regulated by a PROTEIN instead of a transcript — the observable signature is the two-tier delay (mRNA present, protein still below threshold; tests/grn_translates.op proves the gate opens later than one-tier). (Term audit: [MN-translates].) -
occupy(reg-bio-2, D2b) — occupancy repression. An inhibiting edge markedoccupycomposes MULTIPLICATIVELY:child *= 1 − influence— the thermodynamic Kⁿ/(Kⁿ+Rⁿ) survival form. Two partial repressors survive more than subtractive repression would allow (1 × 0.6 × 0.6 = 0.36 vs 1 − 0.4 − 0.4 = 0.2), repression can never overshoot, and near-full occupancy approaches (never crosses) zero. Canonical edge order:strength → threshold → hill → any → occupy → sum. Legacy edges keep the subtractive-once form (bit-identical default). (Term audit: [MN-occupy].) -
sum(reg-bio-2, B7) — synergistic pooling.sumon a thresholded activating edge makes it a POOLED member: edges targeting the same gene with the same (threshold, hill) pool their weighted inputsP = min(1, Σ strength·level)and ONE Hill function of P drives both the gate and the fire influence. Two sub-threshold inputs (0.3 each, threshold 0.5) fire together — enhanceosome synergy that individual AND members can never express — andhill 2makes the pooled response super-additive (pooled influence > sum of individual influences). Groups act as conjunctive members: the gate opens iff (every AND member passes) OR (any OR member passes), where a pooled group passes iffP ≥ t(with the enhance boost). Message order: individual members first, then pooled groups in declaration order. -
decoy d for tf capacity c;(reg-bio-2, C11) — competitive titration. A decoy node absorbs its regulator without producing output: every regulation read oftfsees the free fractionmax(0, level(tf) − c·level(d)). Overexpressing the sponge closes gates that were open; emptying it restores the free fraction. Deterministic, no RNG [MN-decoy]. -
ligand x;+bind tf inducer lg k v;/bind tf cofactor lg k v;(reg-bio-2, A4) — small-molecule allostery. Ligands are metabolites, not genes:ligand_set("iptg", 0.9)sets a pool (clamped 0..1),ligand("x")reads it, and the.cellkeyligand.<name> = fis the bath default (the runtime pool wins). A binding record modulates the regulator's DNA-available fraction at EVERY regulation read:occ = L/(k+L)(k defaults 0.1); an INDUCER reduces affinity — free × (1 − occ) (allolactose on LacI: binding relieves repression); a COFACTOR increases it — free × occ (tryptophan on TrpR: binding enables repression). THE POLARITY IS THE BIOLOGY: lac's inducer turns a repressor OFF, trp's corepressor turns it ON, and in both cases the regulator's LEVEL never changes — allostery, not dilution. A ligand named as an edge SOURCE gates calls straight from its pool — a protein-free metabolite-sensor gate (nogrn_fireneeded; the retired label: [MN-@riboswitch]). tests/lac_gate.op proves the canonical IPTG + cAMP dual control; tests/trp_attenuator.op proves the corepressor polarity mirror. -
attenuates(reg-bio-2, A5) — RNA-level attenuation. An edgex attenuates y threshold t;vetoes like an inhibitor but reports the RNA-level mechanism: "attenuator … (leader terminated)" — the OUTCOME of ribosome-coupled leader attenuation (trp operon), not TF occlusion. (Term audit: [MN-attenuates].) -
decay_clock(n, f?)(reg-bio-2, C2) — time-driven decay. The call clock is the language's in-model timebase;decay_clock(n, f)fires one GRN decay step (fraction f, default from.cell [grn] decay) every n calls, and the translation layer integrates on the same ticks. Decay now runs as expression time passes [MN-decay_clock]..cell [grn] decay_calls = Nis the config form;decay_clock(0)restores the event-driven contract (byte-identical default). (Term audit, C4: [MN-decay_clock].) -
operon Name { cistron rbs r; cistron2; }(reg-bio-3, A1/A7) — the polycistronic transcription unit. The namesake construct: ONE promoter drives N cistrons on ONE polycistronic mRNA. A call to ANY cistron is a transcription attempt of the WHOLE unit: edges targeting the UNIT gate every member first (induction acts on the unit's promoter — one edge opens or blocks all cistrons), then the cistron's own edges apply as usual. The unit pass short-circuits: its message wins over per-cistron messages ("grn gate: 'lacZ' call suppressed (operon 'lac': …)"). Anattenuatesedge targeting the unit is the trpL leader: when it fires, NO cistron is transcribed (the whole polycistronic mRNA is lost). Per-cistronrbs ris the translation-efficiency multiplier (Shine-Dalgarno strength, clamped 0..=1, default 1.0 — distinct from edgestrength, a binding weight): everytranslatesedge sourced at a cistron has its rate scaled by the cistron's rbs, so the lacZYA stoichiometric gradient lives in the two-tier protein nodes (tests/operon_unit.op proves pz:py:pa = 1.0:0.6:0.3). Transcriptional polarity (reg-bio-3, re-weighted loop-9): an upstream cistron that is target-less RISC-degraded or methylated-past-threshold derates transcriptional read-through — every downstream cistron's protein integration is multiplied by the upstream members' expected factor (a fully-captured or methylated upstream member contributes the fullpolarity; a partially-captured one contributes proportionally — "blocks" would be the old binary rule). Rho-dependent termination (loop-10, F-7 — SHIPPED, opt-in): when.cell rho.termination = true, the pinned funnel extends to RISC → toggle → GRN → methylation → riboswitch → promoter → RHO. After the promoter gate, a called cistron scans its upstream members in transcriptional order: an upstream cistron whose translation fails on THIS attempt — methylated-past-threshold (deterministically naked) or a target-less RISC silence whose per-call capture draw fires — is NAKED RNA: Rho loads, chases, and terminates the rest of the transcript for THIS call with probability1 − (1−catch)^d(.cell rho.catch, clamp 0..1, default 0.5; d = the naked runway in cistrons between the failure and the reader — the FURTHER downstream the reader, the MORE time Rho has had to catch up, so the probability GROWS with distance; per-cistron compounding by repeated multiply — never powf). The terminated call returns null with arho terminated: transcript lost at '<g>'note and counts NOTHING (no counters, no transcript, no queue — a terminated call is not expression). The first naked-AND-unshielded member decides;catch = 0.0is read-through-certain,catch = 1.0termination-certain. Composition rule (R10 W2): under Rho ON the D1 polarity factor is IDENTITY — D2 resolves the upstream failure per call (terminate or read-through), so a SURVIVING transcript is whole and translates at full rate; the D1 expected-value derate applies only when the Rho layer is off (it is the expectation-form stand-in for the same loss). (Not modeled: [MN-operon-unit].) Ribosome-queue coupling shield (loop-10, F-8): every successful unit call grows EVERY member's queue by its rbs (the Shine–Dalgarno initiation propensity — one polycistronic transcript loads every cistron's ribosomes), capped at.cell ribosome.queue_cap(default 1.0); every integration tick drains each queue by.cell ribosome.drain(default 0.5); a queue ≥.cell rho.queue_floor(default 0.5) occludes the rut sites — Rho cannot fire behind a translated cistron (the transcription–translation coupling trait; captured calls grow nothing — their transcript never existed). Honesty note (R10-M4/M6): the register is an INITIATION-FLUX abstraction, not elongation-ribosome pile-up — occupancy shields the failure POINT, not the naked runway past it; both boundaries are pinned in tests/granted/rho_queue_shield.op. The queue register and resolved knobs ride the snapshot (§13 — worker cells fold the parent's termination math exactly). ENTROPY: flag off = zero draws (bit-identical legacy); flag on draws only where 0 < p < 1 (member order) and where 0 < q < 1 (one catch-up draw) — the C9 p∈{0,1} no-draw discipline. The granted-lane proofs (tests/granted/rho_*.op under explicit operator cells) pin the deterministic extremes, the exact seeded distance-decay counts, the shield's ≥-floor boundary, drain re-exposure, and worker parity; harness.py's granted targets pin the byte-exact ENTROPY-STREAM parity on both cores; rt_p14e holds the 1500-cistron load shape contained. Methylation-past-threshold contributespolarityoutright; a target-less RISC silence with per-call capturep = 1 − Π(1−s_i)^sites_icontributessurv + (1−surv)·polaritywithsurv = Π(1−s_i)^sites_i— a 1%-capture silence now derates by 1%, not by the full0.5^1the old binary-existence rule imposed (polaritydefault 0.5,.cell operon.polarity; silent at the note layer, observable through levels; no randomness consumed — this is the expected value). Cistron ORDER is load-bearing: it determines the RBS gradient and polarity exposure. Membership is by name (cistrons may be defined after the unit); redefinition replaces (last wins); a cistron owned by another unit is ignored with a note; a unit without targeting edges is inert (declarative). Each call that passes every gate makes ONE transcript of the unit (fingerprint().transcripts, §14 — a suppressed call is not expression). Worker cells inherit the unit registry (§13). (Term audit, 13a: PROKARYOTE-specific — [MN-operon-unit].) -
passage(n)(reg-bio-3, B2/B6) — cell divisions and epigenetic inheritance [MN-passage].passage(n)advances the cell n divisions: every methylation level is multiplied by.cell methyl.maintenance(default 0.5 = pure dilution; 1.0 = perfect maintenance, marks stable; 0.0 = instant loss) with half-down rounding on the 0..=3 lattice — a diluted mark never reads as MORE repressed (3 → 1 → 0 at 0.5; 1.5 rounds down to 1).generationcounts divisions (saturating u64;fingerprint().generation, §14) — spawn is a thread, not a division, and never touches it.nis clamped to 1,000,000 with a note [MN-passage]. tests/passage_dilution.op proves the dilution ladder and the generation counter. -
m6a_write(g, n?)/m6a_erase(g, n?)(reg-bio-3, B3) — quantitative m6A site density. The @m6a mark is now a LEVEL 0..=3 per gene (Dam-style analogy per the @m6a term audit — [MN-m6a-write]; no eukaryotic mRNA-reader drift): every executed@m6a-marked definition deepens the level (+1, capped 3), and dispatch/redefinition resistance requires level ≥ 1 — the legacy bool is exactly the {0,1} sub-lattice, so mark/unmark programs are bit-identical while the builtins make the mark quantitative: write adds writer-complex dose (clamped 3), erase removes eraser dose, and an ERASED mark truly releases the redefinition (the old bool could not)..cell m6a.decay ferases site density on every decay-clock tick (level × (1−f), half-down) — higher density persists longer. Standalone cadence (loop-9): with no GRN decay clock configured,m6a.decayruns on its own cadence — everym6a.decay_callscalls (default 1) — instead of being a silent no-op behind the GRN tick's early returns. Levels live on the regulation snapshot (workers inherit). -
splice_shift(root, variant)(loop-9, F-4) — runtime splicing-factor regulation [MN-splice_shift]. The shift slots into variant selection between the operator pins (.cell variant.<root>→cli.variant) and the @m6a mark: a bound factor beats a basal inclusion bias, the operator still has the final word. The builtin rebinds the splice root TRANS — up the environment chain, so every future transcript of the root uses the shifted variant (in-flight calls finish on their resolved definition — no mid-call body swap, ever); returns the now-active variant name; soft-fails (rung-4 note + Null/current name) on unknown root/variant; zero RNG; rides the snapshot (workers freeze the shift at spawn). tests/splice_shift.op. -
@riboswitch ligand off|on threshold t(loop-9, F-5) — the CIS riboswitch. The sensor is per-gene while the metabolite pool is cell-wide (ligand_level) — the cis requirement [MN-@riboswitch].offclass (TPP/purine/SAM): ligand bound (level ≥ t) → terminator hairpin → transcription OFF.onclass (adenine/glycine activators): unbound → RBS sequestered → OFF. Gate order extends the pinned funnel to RISC → toggle → GRN → methylation → riboswitch → promoter → RHO (loop-10, opt-in) (DNA-level chromatin first, then the RNA-level element, then coupling-dependent termination) on both call paths; a terminated transcript is not expression (null before the counters).@acetylatedoes NOT immunize — the RNA hairpin is downstream of chromatin. The trans ligand EDGE remains the protein-free metabolite-sensor analog (the retired label: [MN-@riboswitch]). tests/riboswitch_cis.op. -
m6A READER fate (loop-9, F-6). The mark is Dam-style resistance at density 1; at density ≥ 2 (
.cell m6a.reader.min_level, default 2) eukaryotic READER fate engages ontranslatesedges: YTHDF2-like decay routing multiplies the production term by1 − m6a.reader.decay(default 0.25) and YTHDF1/3-like attenuation addsm6a.reader.translation(default 0.10) to the protein decay — normative orderrate × rbs × polarity × (1−yd2). The {0,1} legacy lattice is bit-identical; resolved knobs ride the snapshot so workers fold the parent's math. tests/m6a_readers.op. -
autoinducer x;+secrete/quorum/quench/quorum_state(loop-9, C8) — quorum sensing: the population layer [MN-quorum]. The signal is EXTRACELLULAR and SHARED — my secretion raises YOUR activation — so the medium is process-global, not cell state: it is deliberately NOT part of the spawn snapshot (workers inherit frozen cytoplasm but a LIVE medium, handed off explicitly like the fuel pool). The pool stores INTEGER molecule counts (u64, saturating cap 1e9): addition is exact, associative and commutative, so concurrent worker secretions COMMUTE — thread interleaving cannot change the pool (the cross-thread answer to the burst_total float-sum lesson).secrete("ahl", n)charges (Int exact; Float floors — never rounds; negative/non-finite clamp to 0 with a note; species auto-registers, cap 64);quorum("ahl")reads the levelmolecules/1e9(ONE division — bit-identical both cores);quorum("ahl", t)compares>= t;quench("ahl", f?)degradesfloor(m × (1−f))(no f destroys all; [MN-quorum]);quorum_state()is sorted-key telemetry. A species named as an EDGE SOURCE is the LuxR·AHL gate — full AND/OR/sum/occupy/Hill composition for free (resolved after explicit grn levels and ligand pools;grn_set("ahl", v)overrides the medium;grn_fire("ahl")refuses with a note — the level lives in the medium). Dilution:passage(n)multiplies every species by.cell quorum.dilutionper division (default 0.5, binary-exact halving; 1.0 chemostat; 0.0 full exchange). Contract: cross-cell reads are deterministic once every secreting worker is JOINED (join-before-read; reads over unjoined workers are outside the contract). No capability gate — the medium dies with the process; there is no persistence and no path out of the sandbox (a future file-backed culture would be default-denyallow.signal). tests/quorum_basic.op (pool semantics), tests/quorum_lux.op (the LuxI positive-feedback all-or-none circuit — the synthase sits IN the operon it activates), tests/quorum_population.op (cross-cell commutation + dilution), tests/quorum_gate.op (AND/sum/resolution-order composition); redteam rt_p15a (species flood + saturation + hot loop) / rt_p15b (the unjoined-read class). -
@copies n(reg-bio-3, C10) — gene dosage. Copy-number variation:@copies n(clamped 1..=64 with a note) amplifies the CONCENTRATION the gene feeds its GRN edges — every regulation read of the gene's level seeslevel × copies, saturating on the 0..1 lattice (dose titration: [MN-@copies]). Copies change transcript AMOUNT, never the call's return value (a call is a transcription event; its return is the per-transcript product) and never the rawgrn_getlevel (the dose is a read-side effect). tests/copies_dose.op proves 0.3×2 opens a 0.5-threshold gate and the return contract. -
Determinism hardening (reg-bio-2, D9):
grn_state()/grn_fire()/fingerprint().callsemit in SORTED key order (HashMap iteration order varied per process — a proof-frame and oracle-parity hazard),burst_totalaccumulates in sorted order (float addition is not associative), and the variance uses explicit multiply (neverpowi). Worker cells derive their RNG stream from the task id (DEFAULT_SEED ^ id·GOLDEN) — the old shared default seed synchronized promoter bursts across cells (perfectly correlated — [MN-telegraph], 12-c C3); sequence-generator cells still share the default stream (loop-8). -
Level hygiene (reg-bio-2, D2c/D7): edge
strengthclamps to 0..=1 at parse with a note (a binding weight is not an amplifier), and fire-phase influence clamps at 1.0 — a level is a concentration fraction; legacy programs (strength ≤ 1) are bit-identical.strength > 1previously pushed levels past saturation, breaking thegrn_setclamp invariant. -
Gate order (reg-r4, pinned; reg-bio extends the tail): for every call, named, value-bound (higher-order), RISC-redirected, sequence-creation, and worker-cell, the gates apply in ONE order: RISC redirect first (silencing rewrites the callee, so it wins), then the toggle gate, then the GRN veto, then the methylation gate, then the promoter gate (the burst draw is the promoter's own stochastic dynamics, downstream of every trans/epigenetic gate). Value-bound gene calls pass the toggle gate like named calls (an unqualified "the pair gates calls"); a ring node named by an edge source contributes its normalized oscillation level only when no explicit
grn_set/grn_firelevel exists (explicit levels win).
Modeling contract. Every mechanism below carries an honesty grade (REAL / APPROX /
ABSTRACTION / SIMPLIFICATION) and an output-meaning statement in
docs/spec/BIO-CONTRACT.md; a change confined to §11/§16 plus that contract is a
bio-modeling change, never a language change (BIO-CONTRACT governance rule 4).
Determinism. Bio evaluation is deterministic and seeded: same binary version + same
source + same seed + same flags + same .cell gives byte-identical output, core and bio
surface alike (docs/spec/DETERMINISM.md). Every stochastic mechanism in this layer draws
from a deterministic seeded stream: the shared mirrored xorshift stream (silence capture,
promoter bursts), worker streams derived from the task id (the D9 bullet below), or
streams keyed to absolute position so a cached replay cannot diverge (repressilator
noise); platform entropy never enters, and the p∈{0,1} no-draw discipline keeps legacy
programs bit-identical.
frame proof { assert(...); ... }, the test reading frame of the file. Skipped byoperon run; executed byoperon test. The same file encodes program + tests (two reading frames over one sequence).frame name { ... }, named frames (metadata/optional scenes); runnable viaoperon run --frame name.operon test [paths...], default paths:tests/recursively. For each file: run its proof frames; a proof failure (Stress burned) is recorded; the suite continues (Total Grammar). A proof must run to completion, an earlyreturn/breakinside a proof fails it ("exited early"), and a proof that exercises zero assertions fails it ("vacuous proof"). Exit code 1 if any failure. Report: files, proofs run, passed, failed, assertions exercised, wobble notes count. The current suite is all green on both implementations, files/proofs/assertions, the granted-lane cell count, and the differential-harness target split are GENERATED, never hand-typed: see docs/STATS.md (regenerate withpython3 scripts/gen_doc_stats.py;scripts/check_docs_sync.pyfails CI on any hand-typed drift).
spawn(f, args?), starts a real OS thread running genef; returns task id (Int).join(id)waits and returns the result (second join → Null + note). Arguments and results cross by serialization (named genes, lambdas, and phenotype instances cross; a running sequence object does not).- Cooperative cancellation (W18).
cancel(id)asks a live task to stop: it sets the task's cancel flag and the task observes it at its next fuel tick boundary, raising the catchable stress[cancelled] "task cancelled". Nothing is preempted and no data is touched; a task that already finished answersfalsewith a note, and so does an unknown id (soft tier).cancel(0)/cancel(negative)are contained the same way. The observed boundary is exactly the fuel tick boundary, with two deliberate exceptions so a worker can act on its own flag: the entry tick of astressconstruct does not raise (the handler gets its window), and while a rescue handler runs the boundary raise is suppressed (the step budget still applies, so a handler cannot spin forever).cancelled()polls the live flag chain (true iff this task or any ancestor was asked to stop); on the host it is always false.task_state(id)reads the lifecycle phase without joining:running|done|cancelledfor a live task, the same answer from the post-join tombstone, and null + note for an unknown id (task_state(0)isdone, the inline-closure task). Cancellation is inherited: a worker's chain carries its own flag plus every ancestor flag, so cancelling a cell stops its whole descent at tick boundaries. A task cancelled before its gene ever reached a tick dies with the[cancelled]stress; a worker inside astress { ... } rescue (e) { ... }window can rescue it and decide its own exit (that completion counts asdone).joinof a cancelled task returns the standard stress map{kind: "cancelled", message: "task cancelled"}. Sequential-oracle note: the oracle runs worker bodies inline at spawn time, so the differential corpus pins only the ordering-free shapes (tests/differential/cancel.op); the timing shapes are Rust-lane evidence under tests/timing/ plus the rt_p20a storm. - Thread panics are impossible by construction: any stress inside the thread is returned as a Stress Map value.
- Memory model note (honesty): values are reference-counted; tasks communicate by args/results, not shared mutable state. Data races on shared globals are prevented by design (closures capture is by value at spawn time for non-local references).
- Worker cells inherit regulation state (reg-r1; reg-r4 inventory; reg-bio-3 extends). A spawned task or sequence cell starts with a copy of the parent's GRN edges + levels, silences (stoichiometric RISC incl. escape bookkeeping), polycistronic operon units (membership, order, rbs, transcript counters), m6A levels, generation counter, gene-dosage registry, methylation counters + threshold, toggle pairs, enhance marks + enhancer dose (reg-bio), the telegraph promoter states + burst counters (reg-bio), and the repressilator ring (node names + the raw ODE levels frozen at the spawn tick + the ring's kinetic parameters, the cell does not live-tick), frozen at spawn time. Worker calls dispatch through the same funnel as the host, so a toggle-repressed allele, a silenced (level ≥ threshold) gene, a GRN-vetoed call, or a burst-off promoter returns null inside the cell exactly as it does outside, regulation is part of the cell, not a host-side illusion. Later parent-side regulation changes do NOT propagate to already-running cells (snapshot semantics). Crossing (§11a): the inherited regulation state is bio-layer state riding the core snapshot membrane (§19d); the membrane rules themselves are unchanged by it.
- Task groups (W15).
wait_all(ids)joins every id in input order and returns the results position-aligned with the input (join semantics per slot: already-joined or unknown ids contribute null + a note, so alignment never shifts). A failing child contributes its stress map like any join; the group call itself never raises.wait_any(ids, ms?)returns the id of the first task in the list whose worker has finished, in wall-clock completion order (ties resolve by scan order), or null + note when the timeout expires first (default 30 s, ceiling 300 s). Completion ORDER is inherently timing-dependent: the sequential oracle answers the first listed id, so the differential corpus pins only the ordering-free shapes (tests/differential/task_groups.op) and the ordering shape is Rust-lane evidence (tests/timing/wait_any.op). Mutexes/atomics remain open on the W15 board entry; channels landed below (W015). - Channels + select (W015).
channel()creates an unbounded FIFO channel, empty at creation, and returns a channel handle (type namechannel, repr<channel>address-free, truthy, identity-compared like the other behavior handles: a copied handle IS the same buffer, two buffers never compare equal).send(ch, v)appends and returns null.recv(ch)returns the OLDEST value; on an empty-and-open channel it blocks; on a closed-and-empty channel it returns null (null is the only closed signal, a sent null is indistinguishable from closed-empty by design; programs that need the distinction use non-null sentinels or select).close(ch)marks the channel closed and returns null; close is idempotent (second close: null + soft note; Go panics here, we contain).select(ch1, ch2, ...)is a BUILTIN, not syntax (theselect { recv a => ... }grammar stays frozen per W036): it polls its argument channels strictly in declaration order and returns the 0-based int index of the first one holding a ready value (a non-empty buffer; a closed channel with buffered values is still ready and receivable). Fairness is by declaration order, documented, never random: the leftmost ready channel always wins, and with several values buffered on one channel select only reports the channel. If nothing is ready and every channel is closed, select returns -1; an empty argument list is the vacuous case and answers -1 with a note. If nothing is ready and at least one channel is open, select blocks until one becomes ready.- The wire membrane. Every payload that enters a channel crosses the SendValue serialization the spawn boundary uses, EVEN on the same-thread buffered path (the queue stores the wire form, so the two cores agree op-for-op and the membrane holds by construction, SPEC §19d): map keys stringify through display (int key 1 is received as "1"), nested behavior handles (genes, sequences, channels) degrade to null, nested phenotype instances arrive as maps carrying the hidden
#phenotypekey, variants ride with their payload. A TOP-LEVEL behavior-handle payload (gene, sequence, phenotype instance, channel) is refused AT SEND TIME with the catchablemembranestress ("send() refuses a phenotype payload; channels carry data, not handles"), a deliberate tightening of the spawn wire where instances cross as maps: a thread that needs to pass behavior passes the gene name (a string), not the handle. A payload nested deeper than the SendValue cap (100k) fails the send with catchableoverflowbefore anything is queued (same discipline as the spawn pre-flight), and every append is charged to the aggregate allocation ceiling exactly likepush, so a send storm is a caught overflow, never an OOM. - The stress families. send/recv/close on a non-channel, and select over any non-channel argument, raise the standard type stress
unfolded.sendon a closed channel raises the dedicated catchable familyclosed_channel("send on a closed channel"), never a crash and never silent loss; check order is membrane, depth, allocation, closed, so a handle payload on a closed channel reportsmembrane. Everything here is catchable bystress { } rescue (e) { }and by?!-style propagation boundaries. - Threads and fuel. Channel handles cross
spawnboundaries LIVE (as a spawn argument, a captured global, or a closure capture): the worker re-binds the same shared buffer, so a host can send and a worker recv across a real OS thread. A channel nested INSIDE a container that crosses spawn degrades to null like any handle (the wire rule above), and a gene that RETURNS a channel across the join boundary degrades to null with the same rule. Blocking is fuel-accounted (wall time is fuel, thesleepshape, SPEC §9b): recv waits in 50 ms slices and select re-polls in 10 ms slices, each slice charging ms×1000 steps against the interpreter budget AND the run-wide shared pool, and each wake observes the cancel chain, socancel()unblocks a parked cell within one slice (the blocked recv raises the catchablecancelledstress and the task exits, no leaked thread), and a never-fed channel drains the run budget into the catchableoverflowstress instead of hanging forever. Same-thread buffered use charges nothing extra and is the deterministic path. - Oracle parity. The sequential oracle mirrors every builtin op-for-op on the buffered path (list FIFO + closed flag + the same wire transform at send time,
channel_wire), so differential programs are byte-identical. The oracle is single-threaded BY DESIGN and never blocks: a program that would block (recv on empty-and-open, select with nothing ready) raises the oracle-onlyblockedstress; the differential corpus never writes one (buffered sends precede recvs/selects), and the blocking shapes are Rust-lane evidence under tests/redteam/rt_p21a.op. Pinned by tests/channels.op + tests/differential/channels.op (byte-identical) plus the redteam payload.
- The wire membrane. Every payload that enters a channel crosses the SendValue serialization the spawn boundary uses, EVEN on the same-thread buffered path (the queue stores the wire form, so the two cores agree op-for-op and the membrane holds by construction, SPEC §19d): map keys stringify through display (int key 1 is received as "1"), nested behavior handles (genes, sequences, channels) degrade to null, nested phenotype instances arrive as maps carrying the hidden
- Structured concurrency:
scope { ... }(W17). Tasks spawned inside a scope block register on the innermost active scope and are joined at block exit, in spawn order, with results discarded, on EVERY flow path: normal fall-through,return/break/continuecrossing the block, and stress. When the block unwinds by stress, cancel-on-error (default on;.cell scope.cancel_on_error = offto disable) asks each registered task to stop (§13 cancellation) BEFORE the reap; the original stress then propagates after the reap, so the caller's rescue sees the kind and message it would have seen without the scope. The reap's cancel is mechanical containment and narrates NOTHING (#59): whether the child already finished or the cancel flag lands first is a genuine thread race (the qemu legs flipped the note text between the tree-walk and VM lanes), so the reap emits no note on any engine — the join that follows is the observable signal. The explicitcancel()builtin keeps its documented notes. A child's own failure is contained as always (its stress map is discarded by the reap, the tombstone saysdone); scope exit never raises because a child failed. Joining a child manually inside the scope still works; the reap's re-join of the same id is contained (null + note). Nested scopes register independently. Sequence cells (§7b) are NOT scope-tracked (they are pulled to exhaustion by their consumer, not joined). Pinned bytests/scope.op+tests/differential/scope.op(byte-identical, stderr now strictly compared — the vm_parity RACY_NOTE normalization is retired) and the Rust-only timing shapes intests/timing/scope_timing.op(cancel-on-error tombstones, blocking reap). - Sequences (§7b) run on the same worker-cell substrate: each sequence body is a worker thread pulling through a rendezvous channel.
fingerprint()returns Map:calls, Map gene → call count (phenotype methods count asName.method).mature, genes called at least once.nascent, genes defined but never called.maturation, mature / total defined genes (the transcript-maturation share).burst, the aggregate burst index: mean over genes of (variance / mean) of per-gene call counts across complete 20-call bins of the run's call clock.burst_by_gene, the per-gene burst indices (0 for a gene with ≤ 1 call or one bin).bursts, Map gene → promoter burst-off count (reg-bio): calls suppressed by the telegraph promoter layer. Empty unless stochastic expression is on; with it on,burstmeasures promoter-driven variance in real expression rather than call patterns alone.transcripts, Map unit → polycistronic transcript count (reg-bio-3, A1/A7): one successful cistron call is one transcript of the unit; suppressed calls count nothing. Empty unless anoperonunit is declared. Sorted-key emission (D9).generation, the division counter (reg-bio-3, B2/B6): advanced bypassage(n)only (spawn is a thread, not a division).
- Method: the run's global call clock is sliced into windows of 20 gene calls; each gene's count per window is a sample. A gene fired in bursts has a high variance/mean ratio; a constitutively expressed one sits near 0. Only complete bins count (a trailing partial bin is dropped).
operon profile f.op, runs instrumented, prints table: gene, calls, exclusive self-time µs (children subtracted), flags (enhanced active repressed), then amature · nascent · maturationsummary and enhance candidates, hot genes (called ≥ 10% as often as the most-called gene) that carry noenhanceannotation.operon profile f.op --chrome trace.json(W096/W097-A) additionally captures per-call spans and writes them as a Chrome Trace Format.jsonthat about://tracing and ui.perfetto.dev render directly: twoph="M"metadata events, then oneph="X"complete event per gene call —{name, cat:"gene", pid:1, tid:1, ts, dur, args:{depth}}—ts/durin microseconds on the same monotonic clock as this table (so trace intervals and the self-time table reconcile),depth= number of live ancestor frames (outermost call = 0). ONE process, ONE thread: the interpreter is a single logical timeline (fibers ride the deterministic virtual clock on that same thread; worker attribution is W098's WORKER-TELEMETRY surface).otherDataself-describes the trace: format tagoperon-chrome-trace, version, source file, unit, clock, and the honest drop accounting — the span log is capped at 1,000,000 entries and past the cap spans are counted indropped_spans, never silently truncated. Capture arms only under--chrome(before load, so top-level calls are in the timeline) and captures nothing otherwise — without the flag there is no capture branch taken at all in normal runs, and inside an aggregate-profiled run the span branch is one dead bool check per call; the aggregate--jsonschema is unchanged. With--jsonthe operator notice rides stderr so the stream stays parseable.- The v2.0 telemetry keys
spliced/unspliced/velocity(and the per-gene variance/mean noise key) are retired;mature/nascent/maturationcarry the same biology honestly (maturation share, not velocity). - Crossing (§11a): the
bursts,transcripts, andgenerationkeys are bio-layer counters (mechanisms in §11); the call clock, the bins,mature/nascent/maturation, and the profile table are core telemetry that touches bio state only where the corresponding mechanism is in play. - The accounting gauges are a different lane:
memory()(arena/intern/alloc counters plus the W013 live-cycle field) is process-cumulative for one binary invocation, not per-run call telemetry; the cycle field's contract lives in §19e.
-
operon debug f.op --break N(W08 phase 1): a statement-level trap in the tree-walk interpreter with a REPL on break:c/continueresumes,s/stepbreaks after the next statement,p EXPRevaluates in the current frame (same notes and stresses as a run),varsdumps the frame chain (values display-truncated),btprints the call chain,qleaves with exit 0. EOF on stdin resumes to completion, so piped sessions are scriptable and never wedge. Workers are separate interpreters and never break. VM-offset breakpoints (DAP adapter, phase 2) are deferred to the A-track. -
--vm/--interp(W09 A2/A6): since v2.6.0 the bytecode machine is the DEFAULT engine for gene bodies (src/vm.rs; docs/vm-design.md §2a);--vmremains accepted for explicitness and--interpopts back to the tree-walking interpreter (the A6 escape hatch, docs/vm-design.md §9). Calls, the gate funnel, capabilities, notes and stress kinds are SHARED code, so output is byte-identical across engines by construction; the differential harness runs every corpus target on BOTH engines against the oracle (default lane + tree-walk lane, both must stay all-green).operon ir f.op(aliasoperon disasm f.op [--json]) prints the OIR1 listing, one line per instruction (idx | mnemonic | operands | line, the line column is the instruction's source-line stamp,-when the insn carries none), deterministic for the same source (stability tests compile mixed programs twice and byte-compare both the text and the JSON listing, W10/W010-A;--jsonemits the same compile pass as a self-describing document with a per-instructionlinefield, pairing withgraph --json). The opcode table, as executed by the machine and documented here — one row per opcode, exactly the machine's mnemonic table (the W010-A stability tests reconcile the two directions: every machine opcode has a row, every row names a real opcode, the counts are equal):
| opcode | operands | meaning |
|---|---|---|
Push |
const idx | push a constant (null/bool/int/float/str) |
LoadName |
name idx | read a name (clone-charge + unbound note, the tree-walk read arm) |
LoadNameQuiet |
name idx | read without note or charge (compound-assign target read) |
StoreName |
name idx | let semantics: rebinding note + define |
AssignName |
name idx | assignment semantics: const check, set, auto-declare note |
AppendName |
name idx, loud | P5 superinstruction: pop the rhs, append it IN PLACE to the named string slot (amortized O(rhs)) — the compiled form of s += x (loud=false) and s = s + <pure rhs> (loud=true). Falls back to the exact replaced sequence (LoadName(Quiet) + Bin(+) + AssignName) for every non-Str shape, unbound names, const targets and slot targets; ceiling and mem-charge stamps ride the Bin/Assign positions |
Bin |
op | pop two, apply the shared apply_binop (exact kinds, messages, line stamps) |
BinImm |
op, const idx | W11 superinstruction: pop lhs, push the constant rhs, apply the SAME apply_binop (fuses the measured Push+Bin pair; identical evaluation order and stress surface, one tick instead of two) |
LoadBinImm |
name idx, op, const idx | W11 superinstruction: read a name (the EXACT LoadName arm: clone-charge plus the unbound note), then apply apply_binop against the constant (fuses the measured LoadName+Push+Bin triple: n < 2, n - 1) |
JmpIfF |
target | pop one, jump when falsy (same truthy() order) |
Jmp |
target | unconditional jump |
EvalExpr |
expr idx | BRIDGE: evaluate the arena expression with the tree-walk, push the result |
BridgeStmt |
stmt idx | BRIDGE: execute the arena statement with the tree-walk |
BridgeStmtInLoop |
stmt, cont, brk, unwinds | bridge inside a COMPILED loop: the bridged statement's flow must reach the right loop (cont/brk patched at loop end; unwinds = scopes opened since the loop top, restored before the jump lands) |
Pop |
discard one value (expression statements) | |
Ret |
return the value on the stack | |
RetName |
name idx | W11 superinstruction: read a name (the EXACT LoadName arm) and return it (fuses the measured LoadName+Ret pair: every return <name> tail) |
Brk |
target | break out of the enclosing COMPILED loop (patched target) |
Cont |
target | continue the enclosing COMPILED loop (patched target) |
EnterScope |
enter a block scope (fresh child env, tree-walk shape) | |
ExitScope |
leave a block scope | |
CallNamed |
name idx, argc | native call (W09): pop argc args, run the SHARED named-call funnel (RISC gate included), push the result |
CallGateRisc |
name idx, degrade target | Z-116: run the RISC silencing gate BEFORE the argument insns (the tree-walk gates pre-args: a degraded call is not expression). Stashes the decision for the paired CallNamed; on a degrade decision pushes the null result itself and jumps past args + call (target = the instruction after the paired call, patched at compile and remapped by every optimizer pass that moves instructions). No silences configured = inert no-op (A5) |
CompoundBin |
name idx, op | Z-116: the compiled back-end of every general compound assignment (x op= rhs). Pops the rhs (its insns already ran — side effects first), raises frozen (E1000) when the target is const (the tree-walk checks is_const between the rhs eval and the binop), reads the target with the exact quiet read AFTER the rhs evaluated (the tree-walk's post-rhs read), applies target op rhs through the shared apply_binop and pushes; AssignName stores after it |
Nop |
a line stamp that executes as nothing (rt_p22a: the tree-walk stamps a call's line at arm entry, before its arguments evaluate, and never re-stamps — the per-insn line table needs a real, no-effect insn to do the same; costs one tick, invisible to output) |
Bridging is the Total Grammar escape: a construct the compiler does not lower natively either evaluates via the tree-walk or degrades to a bridge, nothing is rejected at compile time. The native/bridge split is invisible to program output by construction (shared code paths), which is what the vm lane of the differential harness pins.
operon run f.op [--entry g] [--variant v] [--cell c] [--rna r] [--frame name] [--ires]
[--strict] [--quiet] [--fuel N]
[--allow-read p] [--allow-write p] [--allow-run prog]
[--allow-py module] # substrate-r1
[--allow-net host:port] [--allow-env var] [--allow-all] # §9b
[-- --args...] # dx-r6: everything after `--` is program argv
operon check f.op [--nmd] [--nmd=purge] [--json]
operon test [paths...]
operon fmt f.op [--write] [--indent N] [--quotes single|double] [--width N] [--fmt-config f]
# canonical formatter; wobble-corrected output parses clean.
# --width: soft wrap at parser-safe comma points only (W47-v2;
# docs/specs/FMT-CONFIG.md safe-break contract; AST never changes)
operon fix f.op [--write] [--json] # legacy-surface migrator (W65): s::→dot, synonym canonicalization (the const→let migration is RETIRED, §7d — const is live semantics and a fixer rewrite to let would unfreeze bindings and change program meaning; the const_to_let report field stays always-0 for --json shape stability); dry-run default; meaning-preserving (canonical(fix(x)) == canonical(x), pinned corpus-wide)
operon build f.op --variant v -o out.op [--bundle] [--native]
# W086 contract honesty: build emits specialized source (a
# transformation, not a compiler); --bundle (W87, design:
# docs/design/BUNDLE.md) and --native (W85) refuse with that
# pointer instead of being silently ignored
operon profile f.op [--chrome trace.json]
# --chrome (W096/W097-A): also capture per-call spans and write a
# Chrome Trace Format .json (about://tracing / ui.perfetto.dev);
# self-describing otherData, 1M-span cap with dropped_spans
# accounting; --json stays byte-compatible (notice rides stderr)
operon run f.op --trace-grn trace.jsonl
# W095 GRN tick-stream: every engine update point (grn_fire pulse /
# decay-clock tick) appends one JSONL frame to the file
# {"tick":N,"phase":"fire"|"decay","levels":{sorted map}},
# deterministic output (byte-sorted keys, 6-dp levels), 200k-frame
# cap, drained by the CLI after the run (success or contained
# failure); the interpreter itself performs no I/O. Feeds the
# live regulation visualizer (W095/GenomeLab) from the W094
# static graph export.
operon crispr f.op (--knockout gene | --matrix) [--json]
# knockout: body → return null; then run proofs; report survivors.
# matrix: knock out EVERY top-level gene; viability table (ESSENTIAL if a proof fails)
operon bench f.op [--iters n]
operon version
Crossing (§11a): the bio-layer flags and verbs in the block above (--variant, --rna, --ires, --cell, --trace-grn, check --nmd, operon crispr, build --variant) enter the biology layer (§11); the CLI process rules around them are core.
operon-ls, language server (stdio LSP; lsp-r1 v2 → W45/W46): initialize / shutdown / exit, full-text document sync (ranged edits are ignored, never mis-applied), textDocument/publishDiagnostics (Total Grammar parse notes by rung + tools check phantom calls, resolved CWD-independently: document-relative → CWD → std/ → exe-relative std/ → cargo-manifest std/; repair-carrying diagnostics embed relatedInformation with the canonical interpretation, W46), textDocument/hover with gene/splice signatures and repair provenance (hovering a repaired token shows what the parser decided it meant), textDocument/definition (genes, sequences, splice roots), textDocument/references (W45: word-boundary occurrences, string/comment-aware, declaration included), textDocument/semanticTokens/full (W45: fixed six-type legend, keyword/function/variable/string/number/comment), textDocument/prepareRename + textDocument/rename (W45-v2: same scanner engine as references, every code occurrence becomes one WorkspaceEdit edit, strings/comments/@marks excluded, interpolated {..} expressions included because they are evaluated; identifier-precise, NOT scope-aware, operon check after rename is the honest follow-up; all-or-nothing: invalid/reserved/builtin/same-name/already-taken new names refuse the WHOLE rename as a JSON-RPC error -32001, mirroring W67's operon rna discipline), textDocument/inlayHint (W45-v3: the checker's inferred types for un-annotated let/const bindings, precomputed per document version in the same analyze pass as diagnostics and filtered by the visible range at request time; honest-visibility law — any/never emit no hint because the dynamic escape hatches carry no information, annotated bindings emit no hint because the source already shows the type, and an unknown document answers an empty array, never an error), textDocument/documentSymbol, textDocument/completion (in-file genes with signatures, builtins, keywords, top-level bindings), and textDocument/formatting (the canonical operon fmt engine). Analysis is cached per document version; didClose drops the document and clears diagnostics. operon-ls --explain FILE prints the operon explain Total Grammar report (W38's second door). Contract version + feature list ride the operonLsp handshake block (W62; docs/specs/LSP-VERSIONING.md). Programmer-first by D-008: hovering boost shows gene boost(x) plus its marks (@acetylate, @methylate, @m6a, enhance) and a one-line analogy, gene vocabulary is an intuition aid, never a prerequisite. Zero external dependencies: request JSON is parsed by the language's own json_parse. Shipped in every release archive including the Windows zip; editor setup (Neovim / VS Code / Helix) is in README "Connect your editor".
- Grading (
operon check): start 100; wobble note −2; fallback note −3; NMD premature stop −4; untranslated transcript −1; phantom call −2; unverifiedanchor import−2; hotenhanced genes with codon-optimal names earn up to +6 back; floor 50. Letter: A ≥ 90, B ≥ 80, C ≥ 70, D ≥ 60, else F. check --jsonemits valid JSON: score, letter, note counts,phantoms(undefined called genes),nmdfindings array.--fuel Ncaps the step budget (the run-wide shared pool, default 500,000,000 — §9b); exhaustion raises catchable Stressoverflow.--strict→ exit code 3 if any rung ≥ 3 note occurred (wobble.strict = truein.celldoes the same per run).- Stdout discipline:
promoteprints program output; notes/reports go to stderr (sooperon run f.op > out.txtis clean).--jsonon check/test/crispr emits machine-readable JSON to stdout.
Operon gains a compile-time type checker as an opt-in gate; the dynamic side is untouched (REPL and scripting keep byte-identical semantics, this section adds a pass, never a runtime).
gene add(a: Int, b: Int) -> Int { return a + b } # annotated signature
gene first<T>(items: list[T]) -> T? { ... } # generics, bracketed args
gene m<N: numeric>(a: N, b: N) -> N { ... } # bounds: numeric,
# comparable, trait names
let n: int = 3 let xs: list[str] = [...] let r: result[int, str] = ok(1)
let o: int? = none() # Optional accepts null, the bare type, none(),
# and some(payload-matching) (W01 law + W06 family)
- Check it:
operon run --typed f.op(compile gate; type errors abort BEFORE execution, exit 3) ·operon check --typed f.op(T-series findings in the check stream, diag +--json).--strictrefuses type warnings. - The flagship catch:
x = 10thenx.name()is a compile-time error (T02unknown-member, exact line), not a runtime null-with-a-note. - Inference: unannotated code infers (flow-joined, branch unions); the
binary lattice mirrors
apply_binopexactly (/→Float,//→Int, int**int →Int,+Str/Str→Str).Anyis the escape hatch: dynamic code inside a typed file checks clean, nothing false-positives. - Findings: T01 type-mismatch · T02 unknown-member · T03 arg-type-mismatch · T04 unknown-type · T05 non-exhaustive-match · T06 propagate-non-variant · T07 trait-method-missing · T08 bound-violation · T09 assign-type-change (warning) · T10 return-missing (warning). T06/T09/T10 are warnings; the rest are errors. Codes ride the W041 scheme.
- Exhaustiveness: a match over Option/Result/Bool/union scrutinees must
cover every case;
case _and bind arms cover all; guarded arms cover nothing (the condition may be false). - Traits:
implementscontracts are enforced statically (missing required method = T07 at the phenotype definition); trait names work as annotation types. - Erasure: type parameters (
T) are erased at runtime — the W01 soft contract is a no-op for a bare parameter name; real constraints live in the static checker. Capitalized annotations (Int,Str) are aliases of the lowercase primitives in BOTH cores. - Type aliases (W01-s2):
type Metrics = map[str, float]declares an alias; annotations naming it resolve at PARSE time to the target, so the runtime soft contract is the TARGET's law (the alias statement itself is inert at runtime, the VM bridges it to the tree-walk which no-ops it). Declare before use: an annotation naming an alias BEFORE its declaration stays a plain Named and the typo-armor rule applies. Duplicate alias: a rung-2 note, first declaration wins. A gene's type parameters shadow aliases in annotation position. Mirrored op-for-op by the oracle. - As-built + non-goals:
docs/design/TYPED-MODE.md. Tests:tests/typecheck.rs(41 cases),examples/typed/(good-path corpus +bad_*negatives pinned by the gate).
Moved to the modeling track by W091: docs/spec/MODELING-NOTES.md §3 carries the map
unchanged. SPEC carries the language contract only; the mechanism ↔ biology mapping is a
modeling note, and edits to it are docs-track changes that cannot silently alter behavior
(CONTRIBUTING §8b). The honesty grading of every row lives in
docs/spec/BIO-CONTRACT.md §2; numerical reference pins in docs/spec/VALIDATION.md.
Security model upgrades (§9b amendments):
- Strict symlink resolution. If a path EXISTS, its fully-resolved form (all symlinks followed) must lie inside a grant. The parent-chain probe is used only for not-yet-existing targets. A symlink planted in a granted directory pointing outside the sandbox resolves to its target and is rejected. File effects open the canonicalized path (what was checked is what is touched), and on Unix a write is refused when the target inode has more than one link (hardlink defense).
- Safe-base child environment.
run()spawns children with an EMPTY environment plus OS essentials (PATH, TEMP/SystemRoot family) and the explicitly--allow-env-granted variables. Parent secrets (CI tokens, credentials) cannot leak into effectors. - Thread budget. At most 256 live worker cells per run; exceeding the
cap raises catchable
overflow. Workers get 256 MiB stacks (matching the toolchain's depth headroom) and inherit the host's capabilities AND fuel. A worker panic fails the run with a non-zero exit, never a silent success. - Run-wide fuel.
spawn/sequences no longer mint fresh budgets: host and every worker drain ONE pool (default 500M steps per run).sleepcharges wall-time-proportional fuel (1 step per µs), and entering arescueblock charges 64 steps, neither a sleep loop nor a rescue retry-spin can outrun the budget. - Memory ceilings. String concat and
repeat/str * intshare the 512 MiB ceiling; list concat is capped at 64M elements; live tasks at 4096 (join()them). All raise catchableoverflow. - Server hardening.
serve()applies a 10 s read timeout per connection and caps the live connection table at 256;http_getbounds responses at 64 MiB. - Parser containment. Expression nesting is capped at 4096 (deeper input truncates with a rung-4 note); token peeking is bounds-clamped. Adversarial inputs (million-paren bombs, quote storms) produce diagnostics, never native stack exhaustion.
- Cycle safety. Self-referential lists/maps render with
[...]/{...}markers (repr), serialize the repeated branch asnull(JSON), and compare with identity short-circuit (a structure equals itself). Spawn arguments deeper than 100k fail the spawn with catchableoverflow.
New surface (§10 additions):
- Regex (zero-dependency).
re_match(pattern, s), anchored prefix test.re_find(pattern, s, start?), leftmost match as{text, start, end, groups}or null.re_groups(pattern, s), capture list or null.re_replace(pattern, s, repl), global literal substitution (no$1refs, captures come fromre_groups; empty patterns are rejected likere_match; the result is capped at 64 MiB and every scan is step-charged, dx-r6). Syntax: literals,., classes[a-z0-9^],\d \w \s \D \W \S,* + ? {m,n}, alternation|, groups( )and(?: ), anchors^ $. The backtracking matcher has a hard 2M-step cap: catastrophic patterns raise catchableoverflow(ReDoS-proof by construction). In double-quoted strings quantifier braces must be escaped (\{2,3\}) because{..}is interpolation. - Time (UTC civil calendar).
unix_time(), seconds since the epoch.date_parts(ts),{year, month, day, hour, min, sec, wday}(Sunday=0).date_fmt(ts, fmt),%Y %m %d %H %M %Sexpansion.std/time.oplayers pure duration and calendar arithmetic over these (W33) and addstime_parse_iso(s): parses "YYYY-MM-DD" or "YYYY-MM-DDTHH:MM:SS" into an Int timestamp; fractional seconds are accepted then truncated, a trailingZis accepted, offset suffixes like+01:00are rejected (UTC-only module, W89), and any malformed input returns null. - String repetition.
"ab" * 3and3 * "ab", Python parity, capped by the 512 MiB ceiling. - Join deadline.
join(id, timeout_ms?)returns null and notes when the worker exceeds the deadline (the task stays joinable). - REPL.
operon repl, persistent-expression shell; expressions print their value, definitions persist;:quitexits. Commands::help,:load f.op(execute a file into the session),:proof(run everyframe proofdefined in the session against live state, a proof must complete and exercise ≥1 assertion),:proof f.op(run a file's proofs),:genes,:vars,:reset. Commands are recognized only at an empty code buffer; inside an open block,:text is code.
Semantics pinning (2.1.1): % follows the divisor's sign; // is floor
division and always yields int; comparisons involving NaN are false;
floor/ceil of out-of-i64 floats raise overflow; negative slice indexes
count from the end; sort() returns a new list (immutable-method contract
shared with reverse/slice/map); cyclic imports return the still-loading
module's placeholder map which fills when loading completes (with a rung-4
note); map/filter/reduce/each run callbacks over a snapshot of the source
list (callbacks may freely mutate the original).
This specification is Operon 2.8.0. operon version prints the implementation banner Operon 2.8.0-vm (rust-core, cpp-kernel) (the -vm suffix marks the bytecode machine as the default engine since 2.6.0, §15 — src/main.rs prints the suffix in both engine variants), which matches this document. (sec-r2: the C runtime kernel was deleted, audit A15 proved its intern table was write-only and its raw pointers were the project's one ASan-confirmed memory-safety class; interning now lives in Rust, and the banner no longer claims a c-runtime.)
- Proof frames: green on the Rust core and the Python oracle, files/proofs/assertions are generated in docs/STATS.md.
- Differential harness (Rust core vs Python oracle, program-level stdout): every target byte-exact on both the zero-grant and granted-with-cell lanes, match/diverge counts are generated in docs/STATS.md. The oracle also runs the same proof suite (both implementations green, enforced in CI).
- Red-team suite: 0 breaches is the release gate; the payload count is generated in docs/STATS.md (note-cap, fuel-charge, and output-cap containment verified live on the stochastic-expression, reg-bio-3, and Rho-termination surfaces).
- Playground smoke: expression-core subset in the browser, spec-aligned (unbound reads → null + note).
The contract below states the CURRENT truth, verified behaviorally and mirrored by the
differential corpus (tests/differential/memory_model.op). One rule of thumb: scalar
values copy; containers share.
| Value kind | let b = a |
Mutation through b |
|---|---|---|
| int, float, bool, null | value copy (independent) | n/a (immutable values) |
| str | value copy (immutable; the read allocates a fresh string and is mem-charged, sec-r5 F-9) | n/a |
| list | shared handle (Rc alias) | visible through a (push(b, 3) grows a) |
| map | shared handle (Rc alias, insertion-ordered) | visible through a |
| gene / phenotype / sequence | shared handle | method/state effects visible through both names |
There is no implicit copy-on-write and no implicit deep clone. Programs that need an independent container copy it explicitly (element-wise or via stdlib helpers).
Gene arguments follow the same rule: scalars copy, containers alias. A callee that
pushes a caller's list mutates the caller's list. Callers who need isolation copy before
calling.
Lambdas and inner genes capture the DEFINING environment by reference (the env chain is shared). Mutations of captured variables inside a closure are visible outside it. There is no by-value capture mode.
spawn hands the worker cell a SNAPSHOT: container mutations inside the worker are never
visible in the parent and vice versa (verified: a worker pushing to a parent list leaves
the parent's length unchanged after join). This membrane is what makes Rc safe across
worker cells, nothing aliased crosses the boundary.
Reference cycles (let a = []; push(a, a)) are legal values: equality, repr, and JSON
serialization are cycle-safe (sec-r5 DAG-memoized walks). Lifetime truth: an Rc cycle
lives until interpreter teardown, scripts and short-lived workers never notice; a
long-lived server building unbounded cycles would leak. Chosen strategy (DECISIONS.md
D-013, shipped in stages):
- document the model (this section), no silent reclamation, no determinism surprises;
- SHIPPED (W013):
memory().cyclesis the live-cycle gauge; the accounting contract is below; - reclamation is opt-in through the weak-reference API (§19f), the sanctioned
cycle-breaking mechanism; a
break_cycle()builtin is consciously NOT landed (§19f); a tracing GC is REJECTED for v3, it would break the fuel/mem charge determinism contract (§9b).
Live-cycle accounting contract (W013).
What counts: a container subgraph the engine has PROVEN reachable from itself, where a
container is a list, a map, or a phenotype instance's fields. Variant payloads are walked
through; map KEYS are not walked; gene/env capture cycles are not container edges and
never count. When it increments: at the container mutation that CLOSES the cycle
(push/insert/index write/map insert/field write), detected BEFORE the edge lands by a
bounded walk from the inserted value to the target container. One cycle group counts
once: registering a container whose subgraph already holds a registered member is a
no-op. When it decrements: only on PROOF of death or breakage, re-verified by a bounded
walk on every memory() call: a target whose container was reclaimed (last strong
reference gone) or whose cycle was broken by later mutation is pruned; everything else
persists (D-013: a leak is honest accounting, not a surprise).
Determinism: every detection and verification walk is capped at the same 100k-visit
budget in BOTH engines, visits in the same order, and cuts at the same node. On graphs
beyond the budget the cut is conservative in the gauge's direction: an inconclusive
DETECTION walk does not register (detection is best-effort), an inconclusive VERIFICATION
walk keeps the entry counted (death must be PROVEN, persistence is the default). The
budget is native-walk armor, not fuel: interpreter steps stay the charged currency
(§9b), the cap bounds the per-call native work of memory() and of container mutations
on adversarial graphs (redteam rt_p22a). The gauge is a PROCESS field: cumulative across
files in one binary invocation, like every memory() key, and leaked cycles legitimately
persist into later files' gauges, so proofs pin deltas, never absolutes.
weak(v) hands out a non-owning handle to a container value (list, map, phenotype
instance); strengthen(w) returns the SAME value (same allocation: identity and later
mutations are shared, type() answers the phenotype kind) while any strong reference is
alive, and null (soft note) once the last strong reference is gone. There is no GC:
freeing is immediate at strong-count zero, the null answer fires at the deref, not at
the drop.
- Refusals:
weak()on anything that is not a list, map, or phenotype instance (scalars, behavior handles, channels alike) is a catchableunfoldedstress; so isstrengthen()on a non-handle. Both messages are byte-identical across the engines (pinned in tests/differential/weak_refs.op). - Membranes: a weak handle NEVER rides the wire. A top-level spawn argument and a channel
payload are REFUSED with the catchable
membranestress (a handle's whole meaning is the allocation identity of its target in the creating cell; the snapshot wire could only deliver a lie, a null or a handle re-pointed at a copy). A handle nested inside a container degrades to null on the wire, the same rule every behavior handle has (§19d). - Reclamation decision: weak refs are the sanctioned way to make a large object graph
collectable under D-013: drop the last strong binding, or break the cycle edge yourself
(pop/del/overwrite on a container you strongly hold), and the next
memory()call PROVES the death and prunes the gauge (§19e). What is consciously NOT landed is abreak_cycle()builtin: asking the engine to sever an edge mid-flight would silently mutate a graph that other live aliases can still reach, which breaks identity semantics (a value you hold must never change shape behind your back). Cycle breaking stays an explicit owner operation on edges the program itself controls. - Engine truth vs oracle model: the Rust core answers from real strong counts; the Python oracle has no refcounter and models aliveness as "a name binding for the target is still live on the scope chain". The differential corpus pins only the shapes where the two models agree by construction; a target held ONLY by another container is a Rust-lane shape the corpus deliberately avoids (recorded divergence, not a bug).