[0.2.0] - 2026-05-21
V0.2.0 is the first publicly released line. V0.1.x circulated as a pre-release. The headline additions are cryptographic module signing, the V0.2.0 ISA reset, information-flow labels including negative labels, calibrated WCET cost models from the new keleusma-bench crate, and a substantial documentation reorganization. The wire format is incompatible with V0.1.x; recompile artefacts. The release also retires closures, f-strings, and the text bundled DSL; programs that used those features need rewrites under host-registered natives. See the breaking-change subsections below for the migration surface; the V0.1.x line had narrow adoption so the discontinuity is acceptable.
Cryptographic module signing (R42)
V0.2.0 adds optional Ed25519 signing of compiled bytecode. The mechanism enforces origin authenticity and tamper resistance for modules delivered to embedded targets; the motivating use case is the mothership-to-daughtership UAV scenario where a mothership compiles per-mission scripts and a daughtership verifies them over a bandwidth-constrained link.
- Surface syntax. A new
signedmodifier on the entry function declaration (signed fn main,signed yield main,signed loop main) emitsFLAG_REQUIRES_SIGNATURE = 0x02in the module header flags. The modifier is admissible only on the entry function; asignedmodifier on a helper rejects at compile time with a diagnostic naming the offending declaration. - Wire format. The framing header extends through the existing
header_length: u16field. Unsigned modules retain the 64-byte base; signed modules grow to64 + 8 (signature metadata) + signature_length + padding-to-8bytes. Bytes 64..72 hold the signature metadata (scheme_idbyte, reserved byte,signature_length: u16, reserved u32); the signature payload follows. Ed25519 (scheme_id = 1) is the only V0.2.0 scheme; the byte is the substrate for future migrations to ECDSA / ML-DSA / LMS without an ABI break. - Message convention. The signature covers the full framed buffer with the signature payload bytes and the CRC trailer bytes zeroed. Both signer and verifier reconstruct that view.
- Cargo gate. A new optional
signaturesfeature, off by default, bringsed25519-dalek 2underno_std + alloc + zeroize. Builds without the feature accept unsigned bytecode normally and reject signed bytecode withLoadError::SignaturesUnsupported. Thesignedsurface keyword still parses without the feature so source files remain portable across configurations. - Runtime API. New
Vm::load_signed_bytes(bytes, arena, &keys)for the initial signed load. NewVm::replace_module_from_bytes(bytes, initial_data)for signed hot-swap that consults the VM's trust matrix. NewVm::register_verifying_key,Vm::clear_verifying_keys,Vm::verifying_keys_lento manage the matrix.Vm::newrejects modules carryingFLAG_REQUIRES_SIGNATUREdirectly because the signature info is lost when the Module is decoded; callers use the byte-aware entry points.Vm::new_uncheckedskips the check consistent with its existing trust-skip semantics. - Wire-format API. New
wire_format::module_to_signed_wire_bytes(module, signing_key),wire_format::verify_module_signature(bytes, &keys),wire_format::parse_signature_metadata(bytes, header_length),wire_format::header_requires_signature(bytes). - CLI.
keleusma compile script.kel --signing-key seed.bin -o out.binsigns (requiressignedon the entry function).keleusma run out.bin --verifying-key key.pub(repeatable) populates the trust matrix.keleusma keygen --seed seed.bin --public pub.bingenerates a fresh Ed25519 keypair; the seed file is written with0o600permissions on Unix; existing files are not overwritten. Key file format is raw 32-byte Ed25519 seed and public key respectively. - Hardware verification.
examples/rtosgains akeleusma-signaturesfeature passthrough and a boot-timesetup::run_signed_self_testpath. The N6 binary built with the feature on flashes, verifies an embedded signed fixture at boot viaverify_module_signature, logssigned self-test: verify_module_signature succeeded, and enters the scheduler loop. Without the feature, the firmware behaviour is unchanged.
Documentation lives in R42 (docs/decisions/RESOLVED.md), the wire-format spec (docs/spec/WIRE_FORMAT.md), and the migration matrix for future schemes (secret/SIGNATURE_SCHEME_MIGRATION.md, internal).
Wire-format reset
V0.2.0 publishes a new bytecode wire format and the framing-header version field resets to 1. The format is the result of an ISA audit covering opcode consolidation, opcode addition, encoding regularization, and a section-partitioned body layout. See docs/spec/INSTRUCTION_SET.md for the full opcode listing and docs/architecture/EXECUTION_MODEL.md for the wire format specification.
V0.1.x runtimes cannot read V0.2.0 bytecode. V0.2.0 runtimes cannot read V0.1.x bytecode. The version-field reset signals the discontinuity to hosts; existing artefacts must be recompiled against the V0.2.0 toolchain.
The reset is acceptable because the V0.1.x line has narrow adoption. Future wire-format changes will continue to bump the version field rather than reset it.
ISA changes (V0.1.x → V0.2.0)
- Consolidations.
PushTrue,PushFalse,PushUnit,PushNone,WrapSome, andPopare removed.PushImmediate(u8)replaces the four constant-pushing variants and additionally encodes smallIntliterals (Int(0)throughInt(15)) inline.PopN(u8)replacesPop; single-slot pops emitPopN(1).Op::Add,Op::Sub,Op::Mul, andOp::Negremain in the instruction set but no longer acceptValue::Intoperands; the compiler routesIntarithmetic throughCheckedAdd/CheckedSub/CheckedMul/CheckedNegfollowed byPopN(2)to discard the unusedhighandflagoutputs. The unchecked opcodes retain theirByte,Fixed, andFloatarms. - Drops.
CallIndirect,PushFunc,MakeClosure,MakeRecursiveClosureare removed along with the correspondingValue::Funcruntime variant. Closure-shaped surface expressions and first-class function values are rejected at the type-checker stage with a diagnostic that names the construct. The closure-hoisting compiler pass is retired. - Splits.
CallNativeis removed; every native call compiles to eitherCallVerifiedNativeorCallExternalNative. The two are distinguished by the source-levelusedeclaration:use module::nameproducesCallVerifiedNative;use external module::nameproducesCallExternalNative. The host's registration ABI mirrors the distinction throughVm::register_verified_native(name, fn, wcet, wcmu_bytes)andVm::register_external_native(name, fn, max_invocations_per_iteration); the newVm::verify_native_classificationswalks every call site and rejects a classification mismatch asVmError::VerifyErrorat the entry ofVm::call_function(cached after the first successful walk).Vm::register_nativeandVm::register_fncontinue to ascribe the verified classification, preserving backward compatibility for hosts that do not need external semantics. External natives' per-call WCMU contribution is explicitly zeroed at the verifier handoff; chunk-level integration ofmax_invocations_per_iterationis forward-looking. - Additions. Five bitwise opcodes:
BitAnd,BitOr,BitXor,Shl,Shr. Enables script-level bit manipulation and is a prerequisite for the deferred B19 Multiword work. - Operand narrowing. Control-flow opcodes (
If,Else,Loop,EndLoop,Break,BreakIf) carryu16jump targets instead ofu32. Chunks are capped at 65,535 ops as a hardCompileError; the compiler emits aCompileWarningat 80% of the cap (52,428 ops) prompting decomposition into helpers. The newcompile_with_warningsentry point returns(Module, Vec<CompileWarning>);compileandcompile_with_targetdiscard the warnings for callers that do not need them.
Total opcode count at the close of Phase 5 is 69. The audit's aspirational target of 65 anticipated dropping Add / Sub / Mul / Neg (Consolidation B) and CallIndirect / PushFunc / MakeClosure / MakeRecursiveClosure (Phase 4); Phase 5 additionally retired CallNative in favor of the verified-versus-external split. Consolidation B narrowed Add / Sub / Mul / Neg to Byte / Fixed / Float operand types rather than removing them, because the runtime still needs entry points for those non-Int wrapping or IEEE 754 arithmetic forms. The remaining gap of four opcodes against the aspirational target is the cost of preserving direct script-level support for Byte, Fixed, and Float arithmetic alongside the checked-Int family.
Wire format changes
- Fixed-size opcode records. Each opcode is a 4-byte record: 7-bit
opcode_idplus a 1-bit parity in byte zero, 3 bytes inline operand or operand pool index in bytes one through three. Inline operand fields cover 65 of 69 V0.2.0 opcodes; the remaining 4 reference an operand pool. - Separate operand pool. Compound operands (
(u16, u16)and(u16, u16, u8)) live in 8-byte aligned pool entries with a type tag and parity byte. - Section-partitioned body. The framing header carries offsets and lengths for the opcode stream, operand pool, and auxiliary body. Each section is independently relocatable.
- Framing header grows from 32 bytes to 64 bytes to carry the new section offsets and lengths.
The rkyv-archived encoding survives only for the auxiliary body. V0.2.0 Phase 7a publishes the specification and the wire-format types. Phase 7b ships wire_format::module_to_wire_bytes and module_from_wire_bytes as a parallel route. Phase 7c cuts the default Module::to_bytes / Module::from_bytes / Module::access_bytes over to the wire format: to_bytes delegates to module_to_wire_bytes; from_bytes delegates to module_from_wire_bytes; access_bytes returns &ArchivedWireAuxBody. The VM's zero-copy execution path reads opcodes through the opcode stream section and accesses the auxiliary body via the wire-format header offsets. The legacy 32-byte framing header, the op_from_archived conversion, and the legacy CRC residue constants retire alongside the cutover. Phase 8 drops the Archive, Serialize, Deserialize derives from Module, Chunk, and Op now that the wire format owns serialization; the user-facing FAQ, cookbook, embedding guide, and rejection guide are realigned with the V0.2.0 ISA.
Changed
- Surface text type renamed
StringtoText. The Keleusma surface keyword for textual data is nowText. The former name persistently confused readers given Rust's ownedStringtype. The runtime representation (Value::StaticStr,Value::KStr) is unchanged. Existing scripts must renameStringtoTextin parameter and return-type annotations. Op::Addon text operands is now arena-resident. Concatenation through+no longer routes through the global allocator; the result isValue::KStrallocated throughKString::allocin the arena's top region. Allocation failure surfaces asVmError::OutOfArena. The bundledto_string,concat, andslicenatives likewise produce arena-allocatedKStrresults.Value::DynStrremoved. The global-heap dynamic-string variant present in V0.1.x is gone. All dynamic strings are arena-resident viaValue::KStr. The cross-yield prohibition continues to apply.register_utility_nativesis now arena-aware by default. The non-context variants ofto_string,concat, andslicewere removed.register_utility_natives_with_ctxis retained as a deprecated alias for compatibility.
Changed
- B17 resolved as not actionable. A
cargo-bloatprofiling pass on the STM32N6570-DK bare-metal binary confirmed thatembassy-stm32contributes essentially nothing to the image footprint: 1.9 KiB across all three feature modes, less than 2% of the smallest mode and 0.3% of the full-pipeline mode. The dominant contributors are inside thekeleusmacrate itself:Vm::run(49.8 KiB),compile_with_target(38.4 KiB),typecheck::run_check(23.5 KiB),typecheck::type_of_expr(22.3 KiB), and the parser / monomorphizer methods. Recorded future-pass targets: opcode-dispatch splitting inVm::run, BTreeMap monomorphization consolidation, gating the monomorphizer behind a feature for workloads that do not use generics, and auditing residualDebugimpl retention. None require embassy changes.
Added
- B13 final follow-ons: Byte/Fixed literal coercion, match-arm range narrowing, widening for recursive function summaries. Three independent extensions to the refinement-elision pass. (1) The type checker now coerces a bare integer literal to Byte (when in
[0, 255]) or Fixed<N> in comparison and arithmetic against a counterpart of that type. The literal is wrapped in an explicitExpr::Castnode so the operator's existing same-type dispatch sees matching types and downstream emit produces the correct conversion. Byte-typed refinement predicates (fn in_range(x: Byte) -> bool { x >= 0 and x <= 100 }) now compile, unlocking the Byte natural-range work for actual refinement-elision customers. (2)infer_arg_range_withgained a shadow map parameter and a newExpr::Matcharm that walks each match arm, intersects the scrutinee range with the arm's pattern range, binds a variable pattern to the narrowed range in the shadow, and unions the arm body's range. The summary-pass equivalent ineval_expr_to_rangegained parallelExpr::IfandExpr::Matchhandling. (3) NewInterval::widenandIntervalSet::widenoperators (Cousot-Cousot style) widen growing bounds to infinity. The function-summary pass now seeds every function toIntervalSet::emptyand uses widening afterWIDEN_AFTER_ITERATIONSrounds to converge on recursive bodies. Recursive functions get a sound compile-time summary even though the WCMU verifier rejects them at load time in V0.2; the widening infrastructure is in place for future passes that admit a relaxed WCMU bound or trust-skip recursion. Five new VM tests cover Byte refinement-predicate compilation, Byte literal-coercion elision, match-arm literal-pattern narrowing, match-arm variable-pattern narrowing, and an If-expression summary. 5 new lattice unit tests for widening. 717 lib tests pass. STM32N6570-DK FLASH bumped to 768 KB / RAM 256 KB / HEAP 192 KB to fit the expanded full-pipeline image. - B13 lattice follow-ons closed: Byte natural-range parameter tracking and cross-function return-range summaries. Byte-typed function parameters populate
local_rangeswith[0, 255]; the castb as Wordcarries the range through to a newtype constructor. A new fixed-point pass computes per-function return-range summaries (TypeInfo::function_return_ranges) over the function table at the top ofcompile; the constructor-emit site'sinfer_arg_rangeconsults the summary atExpr::Callsites. Customers includeCounter(some_function())patterns wheresome_function's body is decidable under the lattice. Recursive functions and bodies with unhandled expression shapes (if,match, etc.) remain unsummarized and fall through to the runtime check. Three new VM tests cover function-call summary admission, function-call summary rejection on disjoint, and chained constructor-through-function admission. Byte and Fixed<N> refinement-predicate surface remains blocked by a separate type-checker gap (integer literals default to Word); the lattice infrastructure is ready when the surface supports it. - B13 Tier 3: interval lattice and parameter-range refinement elision. New
keleusma::intervalmodule carries a closed-signed-interval lattice oni64withOption<i64>bounds for the infinity directions. Constructors:full,empty,singleton,at_least,at_most,range. Predicates:is_empty,contains,is_subset_of. Lattice operations:intersect,union. Transfer functions forneg,add,sub. The constructor emission path consults the lattice when constant-fold fails:predicate_true_setdecomposes a refinement predicate body into the interval of values for which it returns true (handlestrue/falseliterals, comparison against the parameter on either side,andas intersection, andnotover a single comparison as operator inversion);infer_arg_rangewalks the argument expression and returns its inferred interval (literals, identifiers vialocal_const_valuesandlocal_ranges, unary negation, addition, subtraction, newtype-to-underlying casts); when the inferred range is a subset of the true set, the runtime check is elided; when the ranges are disjoint, the compile fails. Function parameters declared as refined newtypes populatelocal_rangeswith the predicate's true set, providing the principal source of non-singleton ranges. The lattice is reserved for future use by B12 (helper-function WCMU precision) and B14 (CallIndirect flow analysis). 19 lattice unit tests plus 3 integration tests cover parameter-range elision, disjoint-range rejection, and graceful fall-through on undecidable predicates. 676 lib tests pass. - B13 Tiers 1 and 2: refinement elision extended to constant-folded arguments and let-bound integers. The evaluator at the constructor emit site now folds the argument through a small closure-driven
eval_expr_withthat handles integer arithmetic, comparison, logical operators, unary negation, and identifier lookup. Tier 1 (fold_to_int(arg, &|_| None)) handles arithmetic over literals;Counter(2 + 40),Counter(0 - 1), and similar chains all fold and route through the predicate evaluator. Tier 2 adds alocal_const_values: BTreeMap<u16, i64>table onFuncCompilerthat records each let-binding whose value expression constant-folds to an integer; the lookup closure at the constructor site resolves identifier references through this map. Chained constants (let a = 2; let b = a * 21; Counter(b)) walk the chain via the recorded values. Non-constant lets (let n = neg_one(); Counter(n)) do not register and fall through to the runtime check. Six new VM tests cover the folded-arithmetic admission, the chain-of-lets admission, the compile-time rejection on a folded provable-failure, the compile-time rejection on a let-bound provable-failure, and the runtime-trap fall-through for non-foldable identifiers. 654 lib tests pass. - B13 MVP closed: literal-argument refinement elision. When a refined newtype constructor
Name(literal)is called with a direct integer literal that the compile-time evaluator can prove satisfies the predicate, the runtime call and trap are skipped and the constructor reduces to the inner value. When the literal provably fails the predicate, the compile rejects the construction at the source span with a diagnostic naming the predicate, the newtype, and the argument. Implementation:TypeInfo::refinement_bodiescaches each predicate's(parameter_name, body_expr)for the eligible subset (single bare-variable parameter, no statements, tail-expression body). A small structural evaluator (eval_predicate_at_int) handles literals, identifier substitution, integer arithmetic, comparison, logical operators, and unary negation. Anything outside this subset returnsNoneand the runtime path is preserved. The compile-time path is engaged only for direct-literal arguments; let-bound constants, arithmetic on literals, and values returned from atomic-total functions all retain the runtime check (follow-on work documented in BACKLOG B13). Three new VM tests cover the elision, the compile-time rejection, and continued runtime-trap fall-through for non-literal arguments. 645 lib tests pass. Op::CheckedDivandOp::CheckedModwith(h, l, flag)shape. Closes thei64::MIN / -1andi64::MIN % -1corner cases noted in the V0.2 design pass. The checked construct's/and%paths now emit dedicated opcodes that compute the true result ini128and route through the standard outcome-class arms. ForCheckedDiv, the only overflow case isi64::MIN / -1(true result2^63, decomposed ashigh=0, low=i64::MINwithflag=1); all other divisions route throughokwithhigh=0and the quotient aslow. ForCheckedMod, the corner isi64::MIN % -1(true result0, but the underlying division step overflows); the construct flags it through the overflow arm withhigh=0, low=0. Division by zero continues to trap withVmError::DivisionByZero; the construct does not catch it because the opcode itself fails before arm dispatch runs. The two new variants are appended afterOp::CheckedNegso no existing rkyv discriminants shift; the wire format version stays at 1. Four new VM tests cover the corners. The compiler's checked-construct emission path drops the stamped-zero-flag fallback in favour of the new opcodes.- B15 closed:
Type::Unknownremoved from the type system. The permissive wildcard sentinel that anchored runtime-only dispatch positions in the V0.1 inference pass is gone. The refactor proceeded in three phases. (1) TheType::Newtypevariant changed fromNewtype(String, Box<Type>)toNewtype(String); the authoritative underlying lives inCtx::newtypesand the boxed placeholder was dead weight. This eliminated the largestType::Unknownproducer at the structural resolver. (2) The two remaining producers in the newtype-construction paths now route throughctx.fresh()when the newtype is not yet recorded. (3) Thetypes_compatiblewildcard short-circuit and the variant itself were dropped; every consumer arm collapsed into the surroundingType::Var(_)arm, the cast wildcard became(Type::Var(_), _) | (_, Type::Var(_)) => ..., and standaloneType::Unknown => ctx.fresh()arms were deleted as redundant. Every unannotated position now produces a freshType::Varand inference proceeds uniformly through unification. 642 lib tests pass; bare-metal STM32N6570-DK full-pipeline build verified.
Changed
Type::Newtypevariant shape. Changed fromNewtype(String, Box<Type>)toNewtype(String). The variant carried a placeholder that was never authoritative; the real underlying is inCtx::newtypes. Existing destructuring sites that ignored the underlying field throughType::Newtype(name, _)were updated mechanically toType::Newtype(name).
Added (continued)
- B18 closed: big-number arithmetic worked example, integration test, and guide. New standalone script at
examples/scripts/09_big_numbers.keldemonstrating two adoption patterns for the V0.2 pattern-arm checked construct.mul_full(a, b)reads the high half ofOp::CheckedMul's i128 intermediate directly; worked input2^32 * 2^32 = 2^64yields(high=1, low=0).add_with_carry(a, b)returns a carry-out flag derived from the overflow class; worked inputi64::MAX + 1yields carry=1 with low=i64::MIN. New integration testtests/big_number_arithmetic.rs(big_number_example_returns_1) compiles and runs the script end to end. New guide docdocs/guide/BIG_NUMBERS.mdwalks through the patterns with a chained two-digit-addition example, discusses the signed/unsigned i64 caveats, and cross-references the grammar and language-design sections. - Pattern-matched checked-arithmetic arms with high/low bindings and match-arm guards (breaking syntax change). The numeric-overflow construct's arms are now pattern matches. Each arm carries one outcome class (
ok,overflow,underflow) with patterns in every value position and an optionalwhen exprguard. Pattern positions admit_(wildcard), a bare identifier (binds aWord), or a signed integer literal (matches by equality). Theokarm binds one pattern against the in-range result;overflowandunderflowbind two patterns against the high and low halves of ani128intermediate result. The runtime computes(high, low, flag)forOp::CheckedAdd,Op::CheckedSub,Op::CheckedMul, andOp::CheckedNeg; the high half is the load-bearing value for big-number multiplication and the carry word for big-number addition. The construct exhaustiveness rule shifts from "exactly one of each outcome" to "each outcome's last covering arm is an unguarded catch-all (bare identifier or wildcard in every position)". The pipe-combinedoverflow|underflow => bodyform is removed; rewrite as two arms with the same body. Match expressions in general gain optionalwhen exprguards throughMatchArm::guard: Option<Expr>; the guard is checked in the pattern's binding scope, must evaluate toBool, and falls through to the next arm when false. Exhaustiveness analysis treats guarded arms as non-catch-all regardless of pattern shape. Bytecode-level stack effect on the four checked opcodes is(pop 2, push 3)for the binary forms and(pop 1, push 3)for unary negation; division and modulo continue to use the stamped-zero-flag pattern (high = 0,flag = 0) so the existingOp::Div/Op::Modcover the construct's needs. Compiler dispatch is a virtual loop over arms with literal-pattern equality tests, optional guard evaluation, and variable-pattern binding, mirroring the lowering ofmatcharms. Migration: every existing checked-construct site rewritesoverflow => bodytooverflow(_, _) => bodyandunderflow => bodytounderflow(_, _) => body; the microkernel heartbeat script and the bundled VM/typecheck tests are updated in place. Six new VM tests cover the high-half exposure for multiplication, the literal-high pattern specialization, and the guard fall-through; three new VM tests cover match-arm guard dispatch, fall-through, and exhaustiveness rejection of a guarded catch-all. 642 lib tests pass. - Refined-newtype saturation contracts (closes Item 2 of the V0.2 gap list). Newtype declarations now accept an optional
with saturate_max = Nandwith saturate_min = Mclause:newtype Limited = Word where nonneg with saturate_max = 100, saturate_min = 0;. Thesaturate_maxandsaturate_minkeywords inside a numeric overflow construct are now context-determined. When the surrounding expected type (top of a new expected-type stack maintained by the type checker, pushed by annotatedletbindings and by function return types) is a refined newtype declared with the matching clause, the keyword is mutated in place to a constructor call against the declared literal. The refinement predicate is verified at runtime on that literal exactly as for any other constructor invocation. When the surrounding expected type isWord(the default), the keywords retain the legacyWord::MAXandWord::MINsemantics. Implementation:NewtypeDef.saturate_maxandNewtypeDef.saturate_mincarry the declared values through the AST; parser recognises thewithclause and admits leading-minus integer literals; type checker populatesCtx::newtype_saturate_maxandCtx::newtype_saturate_minduring pass 1a' and consults them at the saturate keyword site after stripping any information-flow labels from the expected type;type_of_expr,type_of_block,check_stmt, andcheck_functionsignatures now thread&mutto permit the AST mutation. Three new VM tests cover function-return context, annotated-let context, and the fall-back-to-Word::MAX/MINpath.docs/spec/GRAMMAR.mdSection 7.5 EBNF anddocs/architecture/LANGUAGE_DESIGN.mdSection "Surface Extensions Added in V0.2" both updated. 637 lib tests pass. - Third gap-closing pass: GRAMMAR.md and MANUAL.md sync, Type::Unknown role clarification.
docs/spec/GRAMMAR.mdgains a new Section 7.5 "V0.2 Surface Extensions" with formal EBNF for newtype declarations, the numeric overflow construct, and information-flow labels, plus worked examples. The keyword table is updated (newtype,where,overflow,underflow,saturate_max,saturate_minadded; the lexer-vs-parser distinction forclassify/declassifyis documented). The operator table gains@(label separator) and|(arm alternation).examples/rtos/MANUAL.mdgains Sections 5.5 and 5.6: the heartbeat task's adoption of the numeric overflow construct is documented with the live snippet and runtime-cost analysis; the remaining V0.2 surface extensions are surveyed with adoption suggestions for newtypes (time-precision discipline), refinement types (input validation), and information-flow labels (telemetry separation). TheType::Unknowndoc comment is rewritten to describe its three current valid roles (newtype underlying placeholder, cast dispatch fallback, types_compatible wildcard) rather than marking it as legacy; removal would touch 26 call sites with risk of inference regressions and is deferred to a dedicated session. Item 4 (refinement-type compile-time elision through range analysis) remains on the backlog. 670 lib tests pass workspace-wide. - Second gap-closing pass: tuple label propagation, newtype extraction, doc sync, microkernel adoption. Per-position information-flow label checking via new recursive
flow_admissiblehelper that descends throughTuple,Array, andOption. Structural unifier now strips outer labels at every recursive entry so nestedLabelledtypes unify cleanly. Three new tests confirm tuples preserve per-element labels, destructuring preserves the labels, and reading a labeled field into an unlabeled slot is rejected. Newtype value extraction viaT as Underlyingcast is now supported: the type checker consultsctx.newtypesto find the authoritative underlying and admits the cast in both directions; the compiler emits no opcode for newtype-related casts because newtypes are transparent at the bytecode level.docs/architecture/LANGUAGE_DESIGN.mdgains a new "Surface Extensions Added in V0.2" section covering newtypes, refinement types, the overflow construct, and information-flow labels. The microkernel's heartbeat task adopts the overflow construct on its counter increment, demonstrating the saturate-on-overflow pattern in a live demonstrator; the std and N6 platform implementations oflog_eventnow surface the counter value. 670 lib tests pass workspace-wide (was 666, +4 new tests). Bare-metal STM32N6570-DK trust-load.textis 146 KB. - QIF gap-closing: arithmetic label propagation, branching taint, additional checked ops, newtype cycle detection. Closes the gaps identified after the design-decision pass. Arithmetic on labeled values (
BinOp::Add,BinOp::Sub,BinOp::Mul,BinOp::Div,BinOp::Mod,UnaryOp::Neg) now unions the operands' labels and applies the union to the result; the previous implementation silently stripped labels at every arithmetic site and constituted a soundness gap. Branching propagation:if cond@L { ... }andmatch scrutinee@L { ... }now propagate the condition's labels to the result throughapply_labels, because an observer of the branch result can infer information about the condition. The branch arms unify structurally and union their own labels in addition. Newapply_labels,strip_labels, andlabels_ofhelpers on the type checker. New checked-arithmetic opcodesOp::CheckedMulandOp::CheckedNegextend the overflow construct's supported set to+,-,*,/,%, and unary-on Word; division and modulo route through the existingOp::Div/Op::Modplus a stampedValue::Int(0)flag because the only arithmetic-overflow case on Word division is thei64::MIN / -1corner which the regular wrap handles. Newtype dependency cycles (newtype A = B; newtype B = A;) are now detected at pass 1a'' and rejected with a diagnostic naming the participating type. Native function signatures with labels (use host::transmit(Word@Open) -> bool) admit labeled-source rejection at call sites; the existing native-signature machinery handles this without further extension. 666 lib tests pass (was 651, +15 new). Cross-feature clippy clean. Module::flags(u8) and theFLAG_EPHEMERALconstant. New header bit set when the verifier proves the module is ephemeral. Reserved bit positions are zero and the runtime ignores any unrecognised bits. Mirrored in the framing header at offset 13.Module::shared_data_bytes(u32) andModule::private_data_bytes(u32). Verifier-populated byte counts for the shared and private partitions of the data segment. Mirrored in the framing header at offsets 24 and 28. Shared data lives in the Vm's slot storage and is host-visible throughVm::set_dataandVm::get_dataas today. Private data lives in the arena's persistent (.data) region added inkeleusma-arena0.3.0 and is not exposed through the host API.sharedandprivatemodifiers ondatadeclarations. New keywords.shared data ctx { ... }is equivalent to today's baredata ctx { ... }and is host-visible; the explicit form documents the intent.private data state { ... }declares a data block that lives in the arena's persistent region and is not exposed throughVm::set_dataorVm::get_data. The compiler and runtime wiring that gives the partition runtime effect lands in phase 4; for now the AST and the parser accept the modifier and the type checker enforces the shape.ephemeralmodifier on entry-point function declarations.ephemeral fn main,ephemeral yield main, andephemeral loop mainassert the producer's intent that the module is provably ephemeral, meaning at every yield or return that crosses the host-VM boundary no arena-resident value is observed, and at every resume or entry no value loaded from arena memory allocated prior to that resume or entry is read. The type checker rejects the modifier on any function whose name is notmain. Modules without the modifier still receive the bit when the verifier infers the property.SlotVisibilityenum (Shared, Private) onbytecode::DataSlot. Mirrors the sourceDataVisibilityat the bytecode layer so the runtime can enforce the host-API boundary without reading the source AST. Serialized as part of the data layout in the bytecode body.- Compiler partitions the data segment by visibility. Shared slots occupy the low index range, private slots the high range; slot indices in
data_fieldsreference the unified space.Module::shared_data_bytesandModule::private_data_bytesare populated from the partition (oneVALUE_SLOT_SIZE_BYTES-sized slot per slot). - R28 relaxed to "at most one data block per visibility class". A program may declare one shared block and one private block. Two blocks of the same visibility remain rejected.
Vm::set_dataandVm::get_datareject private slots. ReturnsVmError::NativeErrorwith a message indicating that the slot is private and not accessible through the host API. The opcode-levelOp::GetDataandOp::SetDatacontinue to admit both kinds, so the script can read and write its own private slots.- Verifier sets
FLAG_EPHEMERAL(underverifyfeature). Sufficient rule:private_data_bytes == 0AND the entry function's signature carries noText(which compiles to arena-residentValue::KStr). The compile pipeline rejects programs that declareephemeralonmainbut whose proof fails, with a diagnostic naming the reason. Modules without the modifier still receive the bit when the inference holds. - Private data lives in the arena's persistent region. The Vm separates the data segment into shared slots (Vm-owned
Vec<Value>) and private slots (rawValuestorage in the arena's persistent region).Op::GetData,Op::SetData,Op::GetDataIndexed, andOp::SetDataIndexeddispatch by slot index relative to the shared count. The Vm carriesshared_slot_count: u16andprivate_slot_count: u16cached at construction. Private slots are initialised toValue::UnitatVm::newand again atVm::replace_module(hot swap). A newDropimpl onVmruns the destructors on every private slot when the VM is dropped so owned resources do not leak. vm::required_persistent_capacity_for(&module) -> usize. New public helper that returns the actual byte count needed inarena.persistent_capacity()to back the module's private data. Differs frommodule.private_data_bytesbecause the latter is inVALUE_SLOT_SIZE_BYTES-sized logical units for WCMU accounting while the former is in actualValuestorage units. Hosts callarena.resize_persistent(required_persistent_capacity_for(&module))before constructing the VM.Vm::newrejects when the arena's persistent capacity is too small. ReturnsVmError::VerifyErrorwith a message naming the required capacity and the call hosts should make. Modules without private data continue to work with the default zero persistent capacity unchanged.Vm::replace_module(hot swap) honours the partition. The host suppliesinitial_dataof the full slot count; the implementation splits the prefix into shared and the suffix into private. The arena's persistent capacity must be at least the new module's storage requirement; if not, replace fails. Old private slots are dropped before the new ones are written.const datadeclaration form. New keyword. Surface syntaxconst data palette { red: Byte = 255, green: Byte = 128 }. Fields carry compile-time literal initializers; field reads compile toOp::Constloads from the per-chunk constant pool; field writes are compile errors. Const data does not allocate runtime data-segment slots and does not appear inshared_data_bytesorprivate_data_bytes. The compile pipeline rejects shared and private data fields that carry initializers (only const data admits them) and rejects const data fields that are missing initializers. The R28 constraint extends to "at most one data block per visibility class", so a program may declare one shared, one private, and one const block simultaneously. Initializers are restricted to scalar primitive literals (Word,Byte,Float,Bool,Text, unit) plus a leading minus on numeric literals; tuple, array, struct, and enum initializers are queued for a later iteration.- Compiler rejects private data blocks where no slot is ever mutated. Diagnostic recommends rewriting as
const data. An unmutated private slot is wasted memory; the programmer almost certainly meant the constant form. Const data is exempt from the check because its values are compile-time and never written; shared data is exempt because the host can mutate slots throughVm::set_datawithout leaving aSetDatain the bytecode. docs/spec/INSTRUCTION_SET.mdresynchronised with the liveOpenum. Closures (CallIndirect,PushFunc,MakeClosure,MakeRecursiveClosure), indexed data access (GetDataIndexed,SetDataIndexed,BoundsCheck), and the numeric-conversion family (WordToByte,ByteToWord,WordToFixed,FixedToWord,FixedMul,FixedDiv) are now documented with operands, costs, and behaviour. The cost summary table extends accordingly. The Data Segment section documents the shared/private/const partition.- Stronger ephemeral inference: parameter-usage refinement. Phase 7 tightens the verifier rule so that a
Text-typed parameter that the entry function's body never references no longer disqualifies the module from ephemerality. The refinement walks the entry function's body AST and checks whether eachText-typed parameter's binding name appears in any expression, statement, match arm, for-loop iterable, or block-tail. UnusedTextparameters are dropped from the signature-uses-text test; return-type analysis continues to be conservative because proving "no Text actually returns" requires dataflow on every return path. The CHANGELOG records this as the practical implementation of the spec rule; further tightening (per-yield arena dataflow over the operand stack) is queued for a future iteration when a real script benefits from it. - Const data: tuple and array composite initializers.
const datafields whose declared type is a tuple(T1, T2, ...)or an array[T; N]may now carry composite initializers in the form(init, init, ...)or[init, init, ...]. Element counts and types are validated against the declared field type at compile time. Indexed read access (lut.table[i]) on a const array field compiles to a constant load followed byOp::GetIndex; writes remain compile errors. Struct and enum initializers stay reserved for a later iteration. - Const data: struct and enum composite initializers.
const datafields whose declared type is a struct or an enum may now carry struct-literal (Name { field: init, ... }) and enum-variant (Enum::VariantorEnum::Variant(arg, ...)) initializers. The parser recognises both shapes inside const initializer position; the compiler validates the type name against the declared field type and rejects mismatches with a diagnostic. Nested composite initializers are admitted through a permissive inner recursion that falls back to type-agnostic literal-to-ConstValue conversion when the precise inner type cannot be determined from the surface context. This completes the const-data initializer matrix for V0.2 (scalar, tuple, array, struct, enum). - RTOS microkernel: production patterns demonstrated. Four new patterns landed on the microkernel to lift it from "language headline" to "adaptable cooperative-scheduling RTOS demonstrator". Per-task WCET budget on
Task::wcet_budget_cyclesrejects load and hot-swap attempts whose declaredmodule.wcet_cyclesexceeds the budget. Per-task supervised-restart policy onTask::max_restarts: onVmError::Haltthe kernel resets the VM's transient state throughVm::reset_after_error, increments the restart counter, and re-dispatches the task until the budget is exhausted. Platform trait gainsfeed_watchdog()(default no-op) called once per scheduler iteration. Kernel gainspost_event(event_id)and an internalenable_event_tick(event_id, period_ms)ticker that simulates an ISR cadence for the demonstrator; tasks wait on events through the existingyield (2, event_id)form, which the scheduler maps toTaskState::WaitingFor. Two new demonstrator tasks:event_listenerwaits on event id 1 (kernel posts every 2500 ms) and logs each wake,faultytriggersDivisionByZeroat every fifth iteration to exercise the supervised-restart path. Bare-metal.textgrew by roughly 3 KB to 140 KB trust-load and 160 KB precompile-plus-verify; FLASH headroom for user code and NPU weights is now 480-500 KB out of 640. floatscargo feature, default on. Gates the surface support for floating-point arithmetic. With the feature off the lexer rejects float literals (3.14,4.75f64) at lex time, the parser acceptsFloatas a type expression but emission ofConstValue::FloatandValue::Floatis compiled out,Op::IntToFloatandOp::FloatToIntarms surface asVmError::InvalidBytecode(the variants stay defined to preserve wire-format stability), the f64 arm inVm::binary_arithand the comparison op are gated out,KeleusmaType for f64is omitted, and theaudio_nativesandstddslmodules are not compiled. The soft-floatcompiler_builtinsroutines (__divdf3,__adddf3,__muldf3) drop entirely once nothing in the runtime references them. On the bare-metal STM32N6570-DK build this is roughly 12 KB; the microkernel disables the feature and now ships in 137 KB trust-load and 157 KB precompile-plus-verify. Default on for backwards compatibility with hosts that need float arithmetic.- Native function signature declarations on
useimports. The surface formuse host::name(T1, T2, ...) -> Rdeclares the parameter types and return type of a registered native at the type-checker level. Call sites validate parameter arity and per-parameter types against the declaration and inherit the declared return type. Native names imported through the bareuse host::nameform continue to take the legacy permissive path that admits any argument types and assigns a fresh return-type variable. Newast::NativeSignaturestruct, parser extension onparse_use_decl, newCtx::native_signaturesmap populated during pass 1c0, and a newcheck_native_call_with_signaturehelper. The microkernel'sscripts/prelude.kelnow declares signatures for all 17 host natives. (B1 foundation; removingType::Unknownentirely remains backlog work.) - Strict schema check on hot swap. New
Module::schema_hash: u32field (CRC-32 of the canonical serialisation of(slot_name, visibility)per slot in declaration order).Vm::replace_modulenow rejects swaps where the new module'sschema_hashdiffers from the loaded module's; the rejection carries the two hash values for diagnostic clarity. A companionVm::replace_module_uncheckedskips the schema check for hosts that intend to swap across incompatible data layouts (e.g. song-A to song-B in an audio demonstrator). Hot-swap test renumbering: the priorhot_swap_new_schema_replacedtest is nowhot_swap_strict_rejects_schema_mismatch, a newhot_swap_unchecked_admits_new_schemacovers the escape hatch, and a newhot_swap_strict_admits_schema_compatiblecovers the same-schema admit path. Bytecode body grew by 4 bytes; golden test regenerated,examples/zero_copy_demo.kel.binregenerated. VmError::categorymethod. NewVmErrorCategoryenum withHalt,SoftScript,SoftHostvariants. Method returns the coarse retry-or-halt policy without storing per-error bytes. Hosts that need finer policy continue to match on the variant directly. The microkernel kernel-error path uses the category to map errors to a single event-code lookup rather thanformat!("{:?}", e), eliminating ~15 KB of float-formatter code from the bare-metal image.- Bare
Option::Nonetype-checker tightening. TheEnumVariantsite forOption::Nonepreviously returnedOption<Unknown>, which could not unify against a concreteOption<T>because the unifier does not narrowUnknownthroughOption's recursive arm. The fix splits theSome/Nonecases:Some(t)uses the payload's inferred type as the inner;Noneuses a fresh type variable that the surrounding context (function return type, let-binding annotation, match-arm sibling, function-call argument position) unifies. Programs of the formfn f() -> Option<T> { Option::None }are now admitted. The previously-blocked positive test for the per-yield arena dataflow refinement (ephemeral_bit_set_when_declared_text_return_never_produced) is re-enabled. - Microkernel flash savings: ~43 KB on STM32N6570-DK. Three coordinated changes shrank the precompiled-bytecode bare-metal
.textfrom 192 KB (V0.2 baseline) to 149 KB trust-load and from 211 KB to 169 KB precompile-plus-verify. Largest contributor (~32 KB): kernel-sideformat!("{:?}", vmerror)calls were replaced withPlatform::log_event(category_code, data), removing the chain of Debug derives onVmError,Value, andConstValuethat pulled incore::fmt::float(flt2dec::dragon,flt2dec::grisu,CACHED_POW10),compiler_builtins::__divdf3/__adddf3, andchar::escape_debug_ext. Release profile gainedpanic = "abort"to drop unwinding tables (~1-2 KB). Three new kernel event codes (EV_KERNEL_VM_ERROR,EV_KERNEL_UNKNOWN_YIELD,EV_KERNEL_TASK_FINISHED,EV_KERNEL_UNEXPECTED_STATE) with per-platform format-string arms preserve diagnostic visibility through the new logging surface. The full-pipeline mode is essentially unchanged (621 KB, was 614 KB) because the source compiler dominates that image; the savings concentrate in the embedded production modes. - Target-scaled
Fixeddefaults for sub-64-bit targets. The surface formFixed(no explicit<N>argument) now resolves through the target descriptor's newTarget::fixed_default_frac_bits()helper rather than the host-hard-codedDEFAULT_FIXED_FRAC_BITS = 32. Cross-compilation to a 32-bit target picks up Q15.16 (frac=16), to a 16-bit target Q7.8 (frac=8), and to an 8-bit target Q3.4 (frac=4); the fraction-bit count is the lower half of the target word width. Threaded into both the type checker (newcheck_with_targetentry point, called bycompile_with_target) and the compiler (newnormalize_fixed_defaultsAST normalization pass that rewrites everyPrimType::Fixed(None)toPrimType::Fixed(Some(target_frac_bits))before downstream consumers read the immediate atOp::WordToFixed,Op::FixedToWord,Op::FixedMul, andOp::FixedDivemission sites). The barecheckentry point keeps the host-default behaviour for backwards compatibility. Two new tests cover the lattice (fixed_default_frac_bits_scales_with_target_word_width) and the end-to-end opcode emission (fixed_default_changes_when_targeting_embedded_16). - RTOS microkernel:
textfeature disabled and logging via registered native. The microkernel runtime keleusma dependency drops thetextcargo feature. Task scripts no longer use string literals or f-string interpolation. Diagnostic logging routes through a newhost::log_event(code: Word, data: Word)native that forwards to a newPlatform::log_event(code: u32, data: i64)method; the per-event format string lives on the host side in each platform implementation (stdprintln!, N6defmt::info!). The script and host agree on the numeric event discriminants by convention through theEV_HEARTBEAT_OK,EV_LED_GPIO_FAIL, andEV_SENSOR_ABOVEconstants insrc/natives.rs.register_utility_nativesis no longer called because f-strings are not used. Two embassy-stm32 features (exti,unstable-pac) are also dropped because the kernel does not exercise them. Resulting bare-metal.texton the STM32N6570-DK: 180 KB trust-load (was 192 KB, -12 KB), 199 KB precompile-plus-verify (was 211 KB, -12 KB). The full-pipeline build at 622 KB is roughly unchanged because the parser and type checker dominate that image. - Stronger ephemeral inference: per-yield arena dataflow refinement. Phase 8 extends the verifier rule that determines whether a module is admissible as ephemeral. The text-size abstract interpretation pass already tracks per-stack-slot
TextSizelattice values through the compiled bytecode in topological call order for WCMU heap-allocation bounding. The pass now also reports per-chunkyields_textandreturns_textflags, computed by peeking the abstract operand stack at everyOp::YieldandOp::Returnand at the implicit end-of-chunk return. The compiler's ephemerality check consults the entry chunk's analysis result: if the entry's declared return or yield type carriesTextbut every concrete boundary-crossing path peeks a non-text value, the module is admitted as ephemeral. The previous signature-only rule disqualified such modules; the new rule is a strict tightening that admits more programs while preserving the soundness invariant. A new public helperverify::module_chunk_text_analyses(&Module) -> Result<Vec<ChunkTextAnalysis>, VerifyError>exposes the per-chunk analysis result for hosts that want to inspect the dataflow result independently. docs/spec/GRAMMAR.mdsynced. Reserved keyword set extended withshared,private,const,ephemeral. Thedata_declandfunction_defEBNF productions extended with visibility modifiers and theephemeralmodifier respectively;const_initializerproduction added.docs/architecture/LANGUAGE_DESIGN.mdsynced. Memory Model section gained Data Segment Partition and Ephemeral Modules subsections documenting the three visibility classes, the storage and lifecycle of each, and the verifier rule with rationale.examples/rtos/MANUAL.mdsynced. New section 5 "Data partitioning: shared, private, and const" walks through the three classes with code snippets from the heartbeat task. The heartbeat script itself was updated to demonstrateconst data cfg { period_ms: Word = 5000 }in place of the previous inline literal.#![deny(missing_docs)]on every published crate.keleusma,keleusma-arena,keleusma-bench,keleusma-cli, andkeleusma-macroseach carry the deny attribute at their crate root. Approximately 357 previously-undocumented public items inkeleusmaandkeleusma-benchgained doc comments covering every public module, type, field, variant, function, and trait method. Two enums (ast::Exprandast::BinOp) and the rkyv-generated archived siblings inbytecode.rscarry per-item#[allow(missing_docs)]with explicit justification because the variants mirror the grammar's productions one-to-one or because the items are macro-generated. Pre-existing broken intra-doc links in eight modules' header doc-comments (referencingMutVisitor,Visitor,Library,Math,Audio,Shell,Interval,IntervalSet,FunctionDef,unify,Subst,Type::Var,chunk_text_heap_alloc,WideWord) were fixed by rewriting them as either fully-qualified intra-doc paths or plain backtick code.keleusma's crate-root doc comment was previously empty; it now carries a multi-section preface (what Keleusma is, a verified quickstart, the Cargo feature overview) sodocs.rs/keleusma's landing page reflects the crate's purpose. CI'sDocjob and the per-crate cargo-doc invocation enforce the lint underRUSTDOCFLAGS="-D warnings -A rustdoc::redundant-explicit-links".
Changed
- Wire format
BYTECODE_VERSIONrolled from 2 back to 1. Pre-release builds briefly used version 2 before this crate achieved public adoption. Bytecode produced under any version-2 build is rejected at load through the CRC trailer mismatch on the new 32-byte header, which is the desired behaviour given there are no known consumers of the version-2 format outside this repository. - Framing header grew from 24 bytes to 32 bytes. The flags byte at offset 13 fills the previous one-byte reservation; the shared and private data byte counts occupy the new 8 bytes between offsets 24 and 32. The header length stays divisible by 8 so the rkyv body remains 8-byte-aligned when the buffer base is 8-byte-aligned. The golden-bytes test was updated; the precompiled
examples/zero_copy_demo.kel.binwas regenerated.
Added (continued)
-
compileandverifycargo features, default on. Two new orthogonal features gate the source-to-bytecode pipeline and the load-time verifier respectively. Withcompileoff, the runtime image no longer contains the lexer, parser, type checker, monomorphizer, and compiler; hosts that ship precompiled bytecode pay only for the VM and bytecode-format machinery. Withverifyoff,Vm::newskips the structural verifier and the resource-bounds check; the host attests that an equivalent verification ran at the artefact-ingestion step. Both features are indefault, so existing consumers see no behaviour change. The compiler still invokes the verifier at the end ofcompile_with_targetwhen both features are on and populates the WCET and WCMU header fields exactly as before; withverifyoff the compiler leaves those fields at 0 (auto). Measured impact on the RTOS-microkernel example bare-metal binary: 614 KB.textwith both features on, 211 KB with verify only, 192 KB with neither. The combinationcompilewithoutverifyis allowed but rarely useful because the resulting bytecode then carries 0 in the header bounds.Vm::new_uncheckedretains its existing semantics under either feature combination;Vm::auto_arena_capacity,Vm::verify_resources, and the free functionauto_arena_capacity_forare gated behindverifybecause their bodies route through the verifier. -
Cooperative RTOS microkernel example. New standalone crate at
examples/rtos/(intentionally detached from the parent workspace because of heavy bare-metal git dependencies). The kernel core isno_std + alloc; every task is a Keleusmaloop mainscript. Two demonstrators ship:three-task-stdon the development host, andthree-task-n6on the STM32N6570-DK throughembassy-stm32,embassy-executor,embassy-time,defmt-rtt,cortex-m-rt, andembedded-alloc::LlffHeap. Three tasks (LED blinker, sensor poller, heartbeat) dispatch cooperatively. ThePlatformtrait abstracts time, sleep, log, and GPIO/sensor/UART/SPI/I2C/ADC access; resource counts live in an associatedPlatformResourcesconstant. DSL natives validate indices againstPlatformResourcesand return a shared script-sideStatusenum (Ok = 0,Err(Word) = 1) whose payload is aStatusErrorCodediscriminant. Verified end-to-end on the STM32N6570-DK on 2026-05-18 with the boot banner, scheduler entry at t≈215 ms, and four heartbeat ticks at five-second intervals across fifteen seconds of capture. Operator manual atexamples/rtos/MANUAL.md; architectural rationale atexamples/rtos/SPEC.md. -
Indexed access for data-segment array fields. Data-segment fields declared as
[T; N]now occupy N consecutive slots and admit indexed read and write throughstate.field[i]andstate.field[i] = value. Nested array types flatten to a single contiguous slab and the script descends withstate.field[i][j]. Three new opcodes carry the access at the bytecode level:Op::GetDataIndexed(base, len)andOp::SetDataIndexed(base, len)perform the indexed slot read and write with a built-in bounds check against the field's total length, andOp::BoundsCheck(bound)is emitted by the compiler between levels of a multi-dimensional access so an out-of-range inner index traps rather than silently addressing a different sub-array.for x in state.field { ... }over a scalar-element data array lowers to a numeric loop issuingOp::GetDataIndexedper element rather than materialising the array as aValue::Arrayon the operand stack. Naked field access against an array field (a barestate.fieldreference outside an indexed or for-in context) is rejected with a diagnostic pointing at the indexed-access form. The data layout for non-array fields is unchanged; scalar and other composite fields continue to occupy a single slot whoseValuerepresentation carries the structure internally. -
OpCost::{Fixed(u32), Dynamic(fn)}enum. Cost-model surface for opcodes whose cost depends on runtime data.CostModel::heap_alloc_costreturns the new type;Op::Addon text operands reportsOpCost::Dynamicbecause the resultingKStringlength is the sum of operand lengths. Hosts that need the pre-V0.2.0 fixed view continue to callCostModel::heap_alloc_bytes, which saturates dynamic costs to zero. The WCMU text-size tracking pass scheduled for V0.2.x evaluatesOpCost::Dynamicagainst anOpCostContextpopulated from the abstract-interpretation lattice. -
textcargo feature, default off. Gates the surface use of strings in scripts. With the feature off, the lexer rejects string literals ("...") and f-strings (f"..."), and the parser does not recognise theTextprimitive type. Thekeleusma-clicrate enables the feature on the runtime dependency so the script runner and the REPL continue to handle strings. Hosts that want the V0.1.x default surface enable the feature explicitly:keleusma = { version = "0.2", features = ["text"] }. Embedding hosts that target very small runtimes get a smaller compiled artifact by leaving the feature off. See the FAQ entry "Enabling text support" for details. -
Vm::new_with_optionsand overflow policy knob. New constructor accepting aVmOptionsvalue with anoverflow_policyfield. The policy decides what happens when a module's declared WCET or WCMU header field saturated tou32::MAXduring compilation.OverflowPolicy::Reject(default) treats overflow as aVmError::VerifyError, preserving the historic strict admissibility.OverflowPolicy::Warnadmits the module and returns aVec<VerifyWarning>describing the overflow.OverflowPolicy::Allowadmits the module silently. The bareVm::newis now a thin wrapper aroundnew_with_options(VmOptions::default())and continues to reject overflow. -
WCMU text-size tracking via abstract interpretation. New
keleusma::text_sizemodule introduces theTextSize::{NotText, Known(u32), Unbounded}lattice with saturating addition, join, and projection into theOpCostContextconsumed byOpCost::Dynamiccost evaluators. Thechunk_text_heap_allocfunction walks each chunk's bytecode linearly, mirroring the operand stack and local variables asTextSizelattice values, and accumulates the dynamic heap cost of every text-producingOp::Add.verify::compute_chunk_wcmucalls this pass and adds its result to the chunk's heap WCMU bound. Programs whose text allocations saturate the bound tou32::MAXare rejected atVm::newunder the defaultOverflowPolicy::Reject. The FAQ exponential-string-concat example is correctly rejected when written as a Stream block; the analysis is conservative for text operations inside loops, behind conditional branches, and against native-produced text. -
Parser recursion-depth limit prevents stack overflow (reviewer report). A deeply nested parenthesised expression (a few thousand parens in release mode, around thirty in a debug build with a 2 MiB stack) used to panic the parser. The parser now bails with a typed
ParseErroratMAX_PARSE_DEPTH = 32. The limit applies at the three recursive entry points (parse_expr,parse_type_expr,parse_pattern) and is chosen to fit comfortably inside the default cargo-test thread stack with headroom for the type checker, compiler, and VM passes that follow. -
Vm::callvalidates argument count and types (reviewer report). Passing too few or too many arguments, or arguments of the wrong type, used to default missing slots toValue::Unitand then surface a confusingTypeErrorat the first use site (cannot add Int and Unit). The runtime now validatesargs.len() == param_countand each argument's runtime type against the parameter's declaredTypeTagbefore any bytecode runs, producing a clearVmError::TypeErrorlikefunction 'main' expected 2 arguments, got 1orparameter 0 expected Word, got Float. The chunk format gains aparam_types: Vec<TypeTag>field that the compiler populates from the function's declared parameter types; primitives map to their concrete tag, composites collapse toTypeTag::Compositewhich the runtime accepts without further checking. -
Vm::resumevalidates the resume value's type (reviewer report). For Stream blocks, resuming with aFloatagainst aloop main(x: Word)signature used to flow the wrong type into the parameter slot and produce a confusing error at the first use site. The runtime now validates the resume value against the loop's parameter type (the same type the yield expression evaluates to) and rejects the wrong type withloop 'main' resume expected Word, got Float. -
Integer literal overflow is now
LexError(reviewer report). Integer literals that do not fit ini64(such as99999999999999999999999999999) previously parsed toValue::Int(0)and silently disappeared. The lexer now reportsinteger literal does not fit in i64with the literal's span at lex time. Decimal, hexadecimal, and binary literal paths all share the typed-overflow rejection; float literals are likewise wrapped in a typedLexError. -
Untyped parameters are inferred from context. Writing
fn main(x) -> Word { x }previously parsed but the inferred parameter type did not flow through to the chunk, soVm::call(&[Value::Float(1.5)])was silently accepted. The type checker now writes inferred primitive types back into the AST after each function body is checked. The compiler'stype_tag_for_paramreads from the filled-inparam.type_expr, so the chunk'sparam_typescarries the inferred tag and the runtime call validator rejects wrong-typed arguments at the boundary. Parameters whose type cannot be inferred fall back toTypeTag::Composite. -
Duplicate function heads are rejected for every category, entry point or not. Two function definitions that share the same name and whose parameter signatures cannot be disambiguated as multi-headed pattern matching previously kept the first head and silently discarded the rest. A multi-headed function whose second head has the same literal pattern as an earlier head was likewise accepted with the second head as dead code. The compiler now applies a
pattern_shape_eqcheck across heads (ignoring guards) and reportsfunction head is dead codeat the offending later definition. The rule applies tofn,yield, andloopcategories alike, and to helpers as well as the entry point. -
Multi-headed entry points are accepted for
fn,yield, andloop. The compile pipeline previously rejected multi-headedloop main(...)Stream blocks with "multiheaded stream (loop) functions are not supported". Multi-headed Stream dispatch is now wrapped inOp::Loop/Op::EndLoopso each matched head canOp::Popits tail value andOp::Breakout to the sharedOp::Resetepilogue. The Stream block continues to contain exactly oneOp::Streamand exactly oneOp::Reset, preserving the structural verifier's invariants. The productivity rule continues to require that every reachable iteration path passes through aYield. -
Modules without an entry point are now rejected at
Vm::new(reviewer report). A module compiled from source that omitsfn main,yield main, orloop mainpreviously surfaced asVmError::InvalidBytecode("no entry point")at the firstVm::call. The constructorVm::newandVm::new_uncheckednow reject the module withVmError::VerifyError("module has no entry point")at the API boundary. TheVm::callcheck is retained as defense-in-depth for the zero-copy entry path that skips the structural check. -
VmError::NotSuspendedfor prematureVm::resume(reviewer report). Callingvm.resume(value)beforevm.call(args)previously surfaced asVmError::InvalidBytecode("cannot resume: VM not suspended"), which conflated API misuse with corrupt bytecode. The runtime now returns the dedicatedVmError::NotSuspendedvariant. -
Source spans threaded through compile-time structural-verification errors (reviewer report). The compile-pipeline rejections for
CallIndirect,MakeRecursiveClosure, and any structural-verifier failure used to attachSpan::default(), which hid the offending source position. The compiler now builds a name-to-span lookup from the original (and hoisted) function definitions and attaches the originating span to eachCompileError, so callers and IDEs can underline the offending construct. -
Bytecode wire format bumped to version 2. The
param_typesfield is the only addition; the V0.1 wire format (version 1) is rejected at load time. Recompile any V0.1 bytecode artefacts to upgrade. -
Option::Some(x) =>andOption::None =>pattern matching. The compiler's pattern-test path now special-casesOption::Noneto use a direct equality check againstValue::Nonerather thanIsEnum(which fails becauseValue::Noneis not aValue::Enum).Option::Some(p)continues to useIsEnumbecause the compiler emitsOp::NewEnumforOption::Some(x)constructions and the runtime convention is thatSome(v)isValue::Enum { type_name: "Option", variant: "Some", fields: [v] }. Type checker'scheck_pattern_against_typeandcheck_exhaustivenesspaths now handleType::Option(_)scrutinees. As a consequence,shell::getenvnow correctly returnsOption<Text>(matching the design choice from the prior round) —Value::Nonefor unset variables andValue::Enum { type_name: "Option", variant: "Some", fields: [Value::StaticStr(value)] }for set variables. -
Standard DSL libraries:
stddsl::{Math, Audio, Text, Shell}. Newkeleusma::stddslmodule introduces theLibrarytrait. Hosts register a bundle of native functions throughVm::register_library<L: Library>(lib: L). Four bundled libraries:stddsl::Math(libm-backed math),stddsl::Audio(DSP utilities),stddsl::Text(text utilities, gated on thetextfeature),stddsl::Shell(shell utilities, gated on the newshellcargo feature). Third-party crates implementLibraryon their own types to ship reusable bundles. The previous direct entry points (utility_natives::register_utility_natives,audio_natives::register_audio_natives) continue to work for backwards compatibility. -
shellcargo feature, default off. New cargo feature that compilesstddsl::Shell. Requiresstd::envandstd::process::Command; therefore incompatible withno_stdbuilds. Thekeleusma-clicrate enables the feature so the CLI runner has shell access. Shell functions:shell::getenv(name: Text) -> Option<Text>(returnsOption::Some(value)when set,Option::Nonewhen unset; companionshell::has_env(name: Text) -> boolfor presence checking when the caller does not want to unwrap an Option),shell::run(cmd: Text) -> (Word, Text)(executes throughsh -c, returns exit code and stdout),shell::run_checked(cmd: Text) -> Text(returns stdout, traps on non-zero exit),shell::exit(code: Word)(terminates the host process). -
Fixed<N>parameterised form. The defaultFixedsurface keyword continues to mean the target-scaled Q-format (Q31.32 on the host).Fixed<N>explicitly pins the fraction-bit count to a literal integerNin[0, 62]. The parser accepts the new generic-numeric-argument syntax;PrimType::Fixed(Option<u8>)carries the count through the AST (Nonefor the default form). The type checker resolves both forms toType::Fixed(u8); the unifier requires equal fraction-bit counts. The compiler emits the new opcodes (Op::WordToFixed,Op::FixedToWord,Op::FixedMul,Op::FixedDiv) with the correct fraction-bit immediate. Three integration tests coverFixed<16>Q15.16 cast and multiply, plus the default-form Q31.32 cast. Target-scaled defaults for sub-64-bit targets are still deferred to a follow-on that threads the target descriptor into the type checker. -
Canonical numeric types Phase 3:
Fixed(Q-format). NewFixedprimitive type, signed Q-format fixed-point. The default form uses target-scaled fraction bits: Q31.32 on a 64-bit host runtime (32 fraction bits), Q15.16 on a 32-bit target (16 fraction bits), Q7.8 on a 16-bit target. The fraction-bit count is the lower half of the word width. Surface keyword recognised by the parser. Arithmetic uses Q-format semantics: Add and Sub are integer add/sub on the fixed-point bits; Mul shifts the i128 product right by the fraction-bit count and saturates; Div left-shifts the i128 dividend by the fraction-bit count before dividing and saturates. New opcodesOp::WordToFixed(u8),Op::FixedToWord(u8),Op::FixedMul(u8),Op::FixedDiv(u8)carry the fraction-bit count as an immediate operand.Value::Fixed(i64)runtime variant.ConstValue::Fixed(i64)compile-time constant. The compiler emits the cast and Fixed-multiply/divide opcodes with a hard-coded 32-bit fraction count matching the host runtime; threading the target descriptor through the function compiler for sub-64-bit targets is a follow-on. ExplicitFixed<N>parameterisation is also follow-on work. Eight integration tests cover round-trip casts, addition, subtraction, Q-format multiply, Q-format divide, negation, and signed comparison. -
Canonical numeric types Phase 2:
Byte(u8). NewByteprimitive type, 8-bit unsigned, range[0, 255]. Surface keyword recognised by the parser. Arithmetic uses wrappingu8semantics (Add, Sub, Mul, Neg wrap modulo 256; Div and Mod use unsigned semantics; comparisons use unsigned ordering). NewOp::WordToByte(truncates Word to low eight bits) andOp::ByteToWord(zero-extends Byte to Word) cast opcodes.Value::Byte(u8)runtime variant.ConstValue::Byte(u8)compile-time constant.KeleusmaType for u8marshalling on the Rust side. Seven integration tests cover cast truncation, wrapping arithmetic, and unsigned comparison. -
Canonical numeric types (Phase 1, hard break). The surface keywords
i64andf64are removed in favour ofWordandFloat.Wordis the target word size (signed, 64-bit on the host runtime);Floatis the target floating-point width (IEEE 754 binary64 on the host). Existing scripts that usei64orf64as type names fail to parse. The numeric-literal suffix forms42i64and3.14f64remain accepted for legacy notation, but they are an inference hint and do not change the surface type names.Byte(8-bit unsigned) andFixed(signed Q-format with target-scaled fraction bits and optionalFixed<N>parameterisation) are introduced in subsequent commits. -
Opaque type support. New
keleusma::opaquemodule introduces theHostOpaquemarker trait and theValue::Opaque(Arc<dyn HostOpaque>)runtime variant. Host applications register Rust types as opaque values from the script's perspective; the script declares the type by name in function signatures (the type checker resolves unknown named types asType::Opaque). Native functions produce opaque values through thehost_arcconstructor; consumers extract a typed reference throughdyn HostOpaque::downcast_ref. Opaque values are host-managed throughArc, have a lifetime independent of the arena, may flow through the dialogue type at a yield, and contribute zero to the script-side WCMU bound. Equality is byArcpointer identity. A small sealed supertrait surfaces the concreteTypeIdwithout requiringcore::any::Any.