Runtime execution metadata contract
May 20, 2026 ยท View on GitHub
Updated: 2026-05-20
Purpose
This note defines the contract that must exist before moving IR executors, escape analysis, evaluator/runtime frames, or collection implementations out of root core.
The neutral core/ir.Program now owns bytecode/slot/analysis shape. Root core.IRProgram still owns executable metadata because it depends on runtime objects, environments, call dispatch, and error behavior. That split is intentional until the runtime contract below is made concrete.
Root executable metadata that must remain in core for now
core.IRProgram still legitimately owns:
[]Objectconstants and capture valuesbindingKeycapture keys*EscapeInfoand safe-mutable/string-builder slot metadata- typed/boxed execution failure caches
- native helper closures
*FnExprreferences forirMakeFntraceName- arity/variadic executable envelopes
These fields are not pure IR shape. Moving them into core/ir would leak root runtime internals into the IR model.
Runtime/channel concurrency contract
Generic channel close/send/receive mechanics now live in core/runtime.Channel[T]; generic pending-value mechanics now live in core/runtime.Future[T,E] and core/runtime.Promise[T]. Runtime-owned Joker wrappers (core/runtime.ObjectChannel, ObjectFuture, ObjectPromise, Agent, and Atom) now keep Object, Error, hashing, type, queue/realization, and atom value semantics outside root; root runtime_kernel.go keeps proc/env registration, alts! vector construction, validators/watches, NIL, core.async namespace glue, and call-helper glue. Delay owns a local promise primitive in core/types to avoid a core/types โ core/runtime cycle while still using the installed callable hook for forcing. Its current contract is:
Channel.Close()is idempotent and safe under concurrent callers.Channel.IsClosed()is the only supported closed-state accessor; callers must not read runtime fields directly.- Sending after close returns false rather than panicking.
- Receiving from a closed channel returns runtime nil with
ChannelReceiveClosed; root proc glue translates that toNILwhere needed.
channel_contract_test.go, core/runtime/channel_test.go, core/runtime/pending_test.go, core/runtime/agent_test.go, core/runtime/atom_test.go, and core/runtime/concurrency_helpers_test.go guard close/idempotency, object-channel result/error propagation, pending-value realization, Future/Promise wrapper behavior, runtime-owned Agent send/await/error behavior, runtime-owned Atom mutation behavior, checked millisecond durations, and parallel panic propagation. This matters for future runtime extraction because async helpers, alts!, futures, promises, agents, delays, atoms, pmap/pcalls, and core send/receive procs must all share the same concurrency semantics.
Required execution boundary
Before moving executors or escape analysis, define an execution/runtime interface that covers:
type RuntimeObjects interface {
Nil() Object
Bool(bool) Object
MakeInt(int) Object
MakeDouble(float64) Object
ToBool(Object) bool
}
type RuntimeCollections interface {
Count(Object) (int, bool)
Nth(Object, Object) Object
Assoc(Object, Object, Object) Object
Conj(Object, Object) Object
First(Object) Object
ToTransient(Object) Object
AssocBang(Object, Object, Object) Object
PersistentBang(Object) Object
}
type RuntimeCalls interface {
Call(Object, []Object) Object
CallSelf(*IRProgram, []Object) Object
MakeFn(*FnExpr, []Object) Object
}
type RuntimeErrors interface {
Errorf(format string, args ...any) Object
Panic(Object)
}
This is a sketch, not an implementation API. The important boundary is ownership: executors can move only when they can call object/collection/call/error operations through a narrow runtime surface rather than importing all of root core.
Migration sequence
- Keep
core/ir.Programas the neutral bytecode/shape model. - Keep root
core.IRProgramas the executable envelope while executor metadata is root-coupled. - Add focused tests for execution metadata behavior before moving code:
- constants/captures (started:
ir_execution_metadata_contract_test.gocovers constants, slots, captures, escape metadata, and neutral-model analysis handoff) irMakeFn(started:ir_makefn_contract_test.gocovers FnExpr retention and current-slot capture semantics)- native helper fallback/failure caches (started:
ir_failure_cache_contract_test.gocovers compile-failure sentinels and native-helper eligibility caching) - escape-analysis safe mutation slots (started:
escape_analysis_contract_test.gocovers call-argument unsafety and string-builder slot classification) - typed/boxed failure flags
- WASM/native native-int conversion boundaries (started:
wasm_host_contract_test.gocovers raw integer result promotion, host argument promotion, and index rejection)
- constants/captures (started:
- Introduce a small runtime execution adapter in root
core. Started:RuntimeExecutionAdapternow codifies root-owned error creation/throwing, capture-slot installation, nested-call argument/capture preparation, typed env-capture installation,FnExpr/slot capture construction, executable metadata reads (slots, bytecode, constants, nestedFnExprs, captures), escape/analysis state, call dispatch, Fn program/arity/WASM/native-helper dispatch, collection/string/cursor operations, mutable-slot candidate detection/conversion, typed-value boxing, typed/boxed execution failure flags, native-helper state, and mem-nth fallback state; boxed, typed, and nanbox executors use it forirThrow, capture prefill,irMakeFn, nested-call call-slot preparation, typed capture installation/clearing, bytecode/constant access, normal and synthetic-call-expression dispatch, Fn internals, equality, collection/string/cursor helpers including typed object assoc/nth fallback, mutable-slot detection/conversion, native helper dispatch, mem-nth retry gating, and failure gating.runtime_execution_boundary_guard_test.gonow rejects directprog.*reach-through, direct Fn internals, direct callable/runtime dispatch, direct cursor dispatch, selected direct collection dispatch, or ad-hoc adapter construction in executor files. - Move escape-analysis helpers only after they depend on neutral model plus explicit runtime facts.
- Move boxed executor, then typed/nanbox executor, only after call/error/frame contracts are explicit.
Non-goals
- Do not export
Object,FnExpr,bindingKey, or frame internals solely to move files. - Do not add compatibility wrappers around old paths.
- Do not move collections/reader/evaluator in the same step as executor migration.
- Do not collapse generated bootstrap migration into executor migration.
Current status
- Neutral IR model: started and guarded by
core/irtests. - Diagnostics/export/WASM/native helper readers: migrated to the neutral model where appropriate.
- Runtime/execution-envelope tests, including WASM/native integer conversion, stable IR function-cache keys,
RuntimeExecutionAdaptererror/function/capture/failure/native-helper/fallback contracts, executor-file adapter reach-through guards, andcore/runtimepackage tests for runtime-owned channel/pending/agent/atom wrappers: gated bymake runtime-contract-check, which is run bymake docs-check. - Executors and escape analysis: intentionally root-bound pending object/frame contracts becoming narrow enough for real package moves. Boxed, typed, inline typed, and nanbox typed executor files now use
runtimeExecfor executable-envelope state, Fn internals, callable dispatch, typed argument boxing, and many collection/string/cursor operations, but they remain in rootcorebecause they still depend onObject,irValue, opcode-local primitive handling, frame-stack result handling, and object conversion internals.