The live logic runtime. Swap functions in a running production process β
inΒ ~500Β microseconds, provably safe, canaried against live traffic, instantly reversible.
For humans, and for AI agents.
Website Β· Demo Β· How itβs safe Β· Category Β· Benchmarks Β· Proofs
Every other tool makes you redeploy. Blaze rewrites live code midβrequest β
and refuses anything unsafe before a single user ever feels it.
10 seconds. One running server. Zero downtime, zero dropped calls.
Swap a live function inΒ ~500Β΅sΒ Β·Β reject broken edits automaticallyΒ Β·Β canary on live trafficΒ Β·Β undo in one call.
βΆ Video not playing? Open it directly or watch the full story on the website.
LaunchDarkly flips booleans. Blaze swaps functions β in ~500Β microseconds, with a proof, a canary, and an undo button. And it is how you let an AI touch production logic without fear.
Blaze is an embeddable, JIT-native scripting language whose headline feature is
sound live reload: change a .blaze file and the running process updates
instantly, state preserved β and the engine proves how the update may be
applied before applying it. A body-only edit hot-swaps one atomic pointer,
lock-free, under concurrent execution. An ABI change is detected, its full blast
radius recompiled, and the transition committed atomically so callers and callees
can never be observed with mismatched signatures. A broken save is proven bad and
refused, last-good still serving. A comment edit is proven to be nothing at all,
and costs nothing at all.
No restarts. No guessed diffs. No silent corruption. Every "instant", "safe", and "proven" below traces to a named test that attacks the guarantee from a second thread while it holds.
examples/living_service is an HTTP
risk-scoring service whose scoring logic is a .blaze program, embedded through
FuncHandles and hot-swapped while thousands of requests per second pour through
it. Its --script mode tells the whole story β over real HTTP, under live load,
every beat asserted:
git clone https://github.com/grloper/Blaze && cd Blaze
cargo run -p blaze-jit --release --example living_service # watch mode: edit rules.blaze, live
cargo run -p blaze-jit --release --example living_service -- --script # the six-beat story, CI-assertedβββ Blaze living-service β scripted run under load βββ
served 51,815 requests across 6 live edits, 0 dropped
[1] body edit β SafeSwap radius {velocity_risk} in 496Β΅s dropped 0
[2] broken save β Rejected 1 diagnostic; last-good serves on dropped 0
[3] runaway while(1) β canary trapped FuelExhausted; auto-aborted dropped 0
[4] risky rule β canary saw 309 divergences; promoted as SafeSwap dropped 0
[5] ABI change β Relink radius {velocity_risk, score} in 1.08ms dropped 0
[6] rollback to gen 1 β Relink radius {..} in 1.28ms dropped 0
steady-state scoring latency: p50 64ns, p99 512ns
all six guarantees asserted β
Six live edits to a service under fire β a retune, a typo, a runaway, a risky
change, an ABI change, and an undo β and not one of 51,815 requests was
dropped or wrongly answered. Each beat is a theorem with a test:
tests/living_service.rs hammers the same
story from four threads.
π½οΈ Hero GIF slot. The animated version of the run above is recorded deterministically from a checked-in
vhstape βdocs/living_service.tapeβ withvhs docs/living_service.tape. Once produced, dropright here.
The shortest honest path β compile a program, call it, hot-swap an edit, and read back what the graph proved about that edit:
use blaze_jit::{LiveRuntime, EditClass};
let rt = LiveRuntime::new("int add(int a, int b) { return a + b; }")?;
assert_eq!(rt.call("add", &[2, 3]), Ok(5));
// Hot-swap the body. The report tells you what the query graph proved.
let report = rt.reload("int add(int a, int b) { return a + a + b; }")?;
assert_eq!(report.class, EditClass::SafeSwap); // one lock-free atomic store
assert_eq!(report.changed, vec!["add".to_string()]); // proven blast radius: just `add`
assert_eq!(rt.call("add", &[2, 3]), Ok(7)); // new code, live, state intactThe full API tour β handles, typed floats, host fns, metrics, rollback, canary, file-watching
use blaze_jit::{LiveRuntime, ScriptHost, Value, CanaryPolicy};
// One-shot: compile a program and call it.
let rt = LiveRuntime::new("int add(int a, int b) { return a + b; }")?;
assert_eq!(rt.call("add", &[2, 3]), Ok(5));
// Hot path: resolve once, call millions of times, lock-free (~50M calls/s/thread).
// The handle survives body hot-swaps transparently and detects ABI changes.
let mut add = rt.handle("add")?;
assert_eq!(rt.call_handle(&mut add, &[2, 3]), Ok(5));
// Floats are first-class through a typed value API β pass an f64, get one back:
let fx = LiveRuntime::new("float score(float x) { return x * 2.5; }")?;
assert_eq!(fx.call_typed("score", &[Value::Float(2.0)]), Ok(Value::Float(5.0)));
// Native functions, callable from Blaze through the same swap table:
extern "C" fn now_ms() -> i64 { /* ... */ 0 }
unsafe { rt.register_host_fn("now_ms", 0, now_ms as *const u8) };
// Hot-swap an edit; the report tells you what the graph proved:
let report = rt.reload("int add(int a, int b) { return a + a + b; }")?;
assert_eq!(report.class, blaze_jit::EditClass::SafeSwap);
assert_eq!(report.changed, vec!["add".to_string()]);
// Observe (opt-in, lock-free) and revert. Rollback replays a past generation's
// source through the same classified, provably-safe swap protocol.
rt.set_metrics_enabled(true);
rt.call("add", &[2, 3])?;
let m = rt.metrics("add").unwrap(); // calls, total_nanos, faults
let _ = rt.rollback(1)?; // back to generation 1, classified
assert_eq!(rt.call("add", &[2, 3]), Ok(5));
// Canary a candidate against live traffic. `call_canary` returns the LIVE
// answer while shadowing the candidate; a wrong/slow one auto-aborts.
rt.canary("int add(int a, int b) { return b + a; }", CanaryPolicy::default())?;
let _ = rt.call_canary("add", &[2, 3])?; // always the live result
if rt.canary_status().map(|s| s.verdict) == Some(blaze_jit::CanaryVerdict::Running) {
rt.promote()?; // classified swap, journaled
}
// Or bind to a file and poll once per frame:
let mut host = ScriptHost::new("game/logic.blaze")?;
loop {
if let Some(report) = host.poll()? {
println!("reloaded: {report:?}");
}
host.runtime().call("update", &[/* dt */ 16])?;
# break;
}call is thread-safe and may race freely with reload β that interleaving is
exactly what the test suite hammers.
Every hot-reload system today β game-engine script reloaders, dlopen
swappers, native patchers β is bolted onto a compiler that doesn't know what
the edit changed. They diff files, guess the blast radius, and hope the ABI
didn't move. When the guess is wrong, the process corrupts or crashes.
Blaze inverts the architecture. It is an incremental compiler first: source
is decomposed into a fine-grained salsa query graph in which every function's
lowered IR depends on its own text and on its callees' signatures β never
their bodies. That asymmetry (the firewall) means the graph computes the
exact invalidation blast radius of any edit as a byproduct of recompiling it:
raw source (input)
β
function_text(f) β per-function firewall node
βββββββββββ΄ββββββββββ
βΌ βΌ
function_signature(f) lowered_dev_ir(f) βββΊ compiled_machine_code(f)
β² β
βββ callee's sig ββββ β callers read the SIGNATURE, not the body
The live runtime then derives the swap protocol from the graph:
| The graph proves⦠| Classification | Commit protocol |
|---|---|---|
| The program has a syntax error, an undefined callee, a call-site arity mismatch, or an undefined variable | Rejected |
nothing is compiled or patched β the previous, known-good generation keeps serving every call untouched. The first load fails construction outright (there's no "previous" to hold) |
| No function's IR changed | NoEffect |
nothing compiled, nothing patched |
| Changed functions all kept their signatures | SafeSwap |
one lock-free atomic pointer store β valid under concurrent execution, because the firewall guarantees no caller's code mentions anything about the callee except its slot |
| A signature changed (or a function was removed) | Relink |
the graph forces every caller into the radius; the whole set recompiles and commits under a quiescence barrier β mismatched caller/callee ABIs are unobservable |
StateMigration is reserved: Blaze functions are pure over their scalar
arguments (i64/f64) and all persistent state lives in the host β which is
precisely why state survives every reload.
Rejected is not a fallback bolted on top β it runs from the same query
graph as everything else. A dedicated checker (blaze_ir::diag) re-walks each
function with the exact statement order and declare-before-use scoping the
lowerer itself uses, and reports every point where the lowerer would otherwise
silently substitute a default (an unresolved name, an unknown callee, a
mismatched argument count). Deleting a function another one still calls is not
a special case: it is simply "now an undefined callee," caught by the same
check, held open by the same protocol.
Live behavior change isn't new β but every existing approach trades away granularity, proof, or speed. Blaze keeps all three.
| Approach | Unit of change | Where it runs | Reload speed | Proven radius | Per-rule canary | Rollback |
|---|---|---|---|---|---|---|
| LaunchDarkly | Boolean / config flags | SaaS eval ($20k+/yr) | flag flip | n/a (no code) | β | flag off |
| OPA / Rego | Whole policy bundle | In-process | whole-bundle reload | β | β | redeploy bundle |
| Sandbox / microVM | Whole script / process | Out-of-process | seconds (spin-up) | β | β | restart |
| dlopen / hot-patch | Native library | In-process | ms, guessed diff | β guessed | β | reload library |
| Blaze | Individual functions | In-process JIT | ~500Β΅s, proven | β per edit | β live traffic | β one call |
The same pipeline that protects a human protects an autonomous editor. Every proposed edit runs a gauntlet before it can reach a real request:
propose ββΆ gate ββΆ offline eval ββΆ canary (live traffic) ββΆ promote / abort ββΆ metrics
reject score vs shadow on a sampled clean β swap
if bad reference slice; caller always bad β auto-abort
set gets the live answer
cargo run -p blaze-jit --example agent_loop # the agent reaches its target; every bad edit is stoppedexamples/agent_loop runs that loop for an
autonomous editor. Its centerpiece is the failure mode that offline testing
cannot catch: a candidate that passes every offline eval, then loops forever
on an input shape the eval set never covered. The canary shadow-executes it on a
sampled slice of live traffic, under the primary's own fuel budget, traps
FuelExhausted, and auto-aborts before promotion. The caller only ever saw
the live answer.
The blast radius of a bad idea is a shadow execution, not an outage.
All numbers below are single-run, release mode, measured on the dev container
this repo was built in β a modest, shared cloud box. They vary run to run; the
ratios and orders of magnitude are the point, and the structural claims (blast
radius bounds compilation, the fast path shares nothing) survive any hardware.
Every table has a cargo run you can reproduce.
Reload latency (bench_reload β a 40-function call chain where every
function transitively depends on the edited leaf; median of repeated edits):
| Event | Latency |
|---|---|
| Full cold compile (β what a restart pays in compilation alone) | 8.5 ms |
Body edit β SafeSwap (radius 1 of 40) |
0.74 ms |
Comment β NoEffect (radius 0) |
0.54 ms |
ABI edit β Relink (radius 2) |
1.9 ms |
cargo run -p blaze-jit --release --example bench_reloadSub-millisecond from reload() to new native code answering calls β ~11Γ faster
than recompiling the program, on a 40-function toy β and a restart also forfeits
all state. Compilation cost scales with the blast radius the graph proves,
not with program size. (In the living-service demo, a SafeSwap under live HTTP
load committed in 496Β΅s.)
Call throughput (bench_calls β a trivial leaf, so this is dispatch cost,
not compute):
| Path | Throughput |
|---|---|
call(name) (lock + string lookup per call) |
~19 M calls/s/thread |
call_handle (resolve once, then lock-free) |
~50 M calls/s/thread |
call_handle, 4 threads |
~188 M calls/s (~94% of linear β nothing shared) |
cargo run -p blaze-jit --release --example bench_callsThe fast path is an arity check, one atomic load (double-checked against the slot's arity so an ABI change is never mis-dispatched), and the indirect call β ~10Γ the "5 M/s/thread" bar a rules engine needs, and it scales near-linearly because nothing on it is shared.
vs. an embedded interpreter (bench_vs_interpreter β the same branchy
risk rule, cross-checked to agree on every input, in Blaze's native FuncHandle
path vs. rhai's compiled-AST call_fn):
| Engine | Throughput |
|---|---|
Blaze call_handle (native JIT) |
~49 M calls/s/thread |
rhai call_fn (AST interpreter) |
~0.96 M calls/s/thread |
cargo run -p blaze-jit --release --example bench_vs_interpreter~51Γ the interpreter on identical logic β and still hot-swappable, which a
compiled dylib is not. That is the "swap functions, don't flip booleans" number.
Canary overhead (bench_canary β what call_canary costs the live path, on
a ~46 ns leaf so the deltas are visible; ns/call is the honest lens):
| Mode | Overhead |
|---|---|
| Idle (no candidate) | +~4 ns/call (one atomic load) |
| Shadowing 1% of calls | +~4 ns/call |
| Shadowing 100% of calls | +~220 ns/call (a full shadow every call) |
cargo run -p blaze-jit --release --example bench_canaryAn idle canary is one atomic load. An active one adds a lock-free counter increment per call (noise for any real handler) plus a shadow on the sampled fraction β and because the sampler is lock-free, mirroring never serializes concurrent traffic. The caller always gets the live answer regardless.
Every "instant", every "safe", and every "proven" in this README traces to a query-graph guarantee with a test that attacks it from a second thread while it holds. Each link below lands on the exact function.
| Claim | Proven by |
|---|---|
| β Hot-swap & blast radius β | |
| Body edit swaps live under concurrent calls, with zero missed calls and zero torn values | live.rs::body_edit_swaps_under_concurrent_execution β a thread calls main() in a tight loop through the swap; every result is old-correct or new-correct |
| The blast radius of a body edit is exactly the edited function | same test: ReloadReport::changed == ["add"], and incremental.rs::body_edit_does_not_invalidate_callers shows the caller was a memo hit |
An ABI edit is classified Relink and transitions atomically |
live.rs::signature_edit_relinks_atomically_under_fire β under the same hammering, observed values are only ever fully-old or fully-new |
A comment edit is proven NoEffect: zero codegen, zero generations |
live.rs::comment_edit_is_proven_no_effect |
| β The reject gate β | |
A syntax error is Rejected; last-good keeps answering every concurrent call |
live.rs::syntax_error_holds_last_good_under_concurrent_execution β hammered like the swap tests; every observed value is old-correct |
| A call to an undefined function never reaches a live process | live.rs::undefined_callee_is_rejected_not_silently_tolerated |
A call-site arity mismatch is Rejected, not silently tolerated |
live.rs::arity_mismatch_is_rejected |
Deleting a function a caller still uses is Rejected, not silently zeroed |
live.rs::removing_a_used_function_is_rejected_and_holds_last_good β both functions keep working exactly as before |
| A defective first program fails construction, not silent misbehavior | live.rs::initial_load_with_a_defect_fails_construction |
| β Resource safety: depth & fuel β | |
| Unbounded recursion aborts the call, never faults the host | live.rs::unbounded_recursion_aborts_with_error_not_a_crash β spin recurses forever; every call returns Err(ResourceExhausted), interleaved healthy calls stay exact |
| The depth guard doesn't false-positive on real recursion | deep_but_bounded_recursion_succeeds + mutual_recursion_is_also_bounded + depth_limit_is_configurable |
An infinite loop aborts with FuelExhausted, never hangs |
live.rs::infinite_loop_aborts_with_fuel_exhausted β while(1){} returns a defined error; the runtime is healthy afterward |
| A runaway can't permanently wedge the runtime | live.rs::relink_commits_after_a_runaway_loop_traps β a thread spins holding the dispatch lock; a concurrent Relink still commits once fuel runs out |
| Fuel bounds shallow-but-explosive recursion the depth guard can't | live.rs::exponential_recursion_is_bounded_by_fuel β naive fib(40) (depth 40, ~3Γ10βΈ calls) is caught by fuel, not depth |
| Fuel doesn't false-positive real loops | live.rs::legitimate_loops_run_under_fuel β a 1000-iteration loop computes correctly under the default budget |
A live-edited x / 0 cannot fault the host |
jit.rs::division_is_guarded_and_cannot_fault_the_process β x/0 == 0, INT_MIN / -1 == INT_MIN |
| β The fast path (FuncHandle) β | |
The fast FuncHandle path is torn-free under concurrent hot-swaps |
live.rs::handle_calls_are_correct_under_concurrent_body_swap β a thread hammers a function through a lock-free handle while it is body-swapped; every value is old- or new-correct |
| A handle survives a body swap yet detects an arity change | handle_survives_body_swap_transparently + handle_detects_arity_change β a stale handle never dispatches a mismatched call |
| β Floats & the type system β | |
float (f64) runs parseβlowerβcodegenβexecute, exactly, and never faults |
float_arithmetic_executes_end_to_end + float_division_never_faults β x/0.0 is a defined Β±inf/NaN, no guard needed |
A float round-trips across call/return boundaries (the i64βf64 bit-cast ABI) |
float_args_and_returns_round_trip_across_calls + raw_and_typed_paths_agree_on_float_bits |
| A float body edit hot-swaps live under a second thread, torn-free | floats.rs::float_body_swap_is_sound_under_concurrent_execution |
Retyping a parameter intβfloat is a Relink, atomic under a second thread |
floats.rs::retyping_a_parameter_relinks_atomically_under_fire β every observation is fully-old float or fully-new int |
A type mismatch is proven and Rejected, never a silent bit-reinterpretation |
diag::tests::mixed_arithmetic_is_rejected (+ wrong-typed return, assignment, argument, and bare-float condition) |
| β Observability, journal & rollback β | |
| Per-function metrics are exact under concurrent callers, lock-free | metrics.rs::counts_are_exact_under_concurrent_callers β four threads hammer one slot; every increment lands |
Every reload (incl. Rejected/NoEffect) is journaled with its class, radius, diagnostics, and latency |
every_event_is_journaled_in_order + rejected_events_are_journaled_but_not_committed |
rollback(gen) reverts through the same swap protocol, torn-free under a second thread |
rollback_reverts_a_body_edit + rollback_is_sound_under_concurrent_execution + rollback_across_an_abi_change_is_a_relink |
| β The canary β | |
| A canary's candidate answer never reaches a caller β even under a storm of concurrent mirrored calls | the_caller_never_sees_the_candidate + the_shield_holds_under_concurrent_traffic |
| A wrong / faulting / too-slow candidate auto-aborts and cannot be promoted | a_diverging_candidate_auto_aborts + a_faulting_candidate_auto_aborts + a_slow_candidate_auto_aborts_on_latency + an_aborted_candidate_cannot_be_promoted |
| Promoting a validated candidate is the ordinary classified swap, seamless under load | a_matching_candidate_promotes_through_the_swap_protocol + promote_is_seamless_under_concurrent_traffic |
| A canary evaluates its candidate under the live program's own fuel/depth budget | canary.rs::a_runaway_candidate_traps_under_the_primary_fuel_budget |
| The canary sampler is lock-free and exact 1-in-N even under a storm of concurrent callers | canary.rs::sampling_is_exact_one_in_n_under_concurrent_traffic |
| β The whole story β | |
| The whole living-service story β six live edits under sustained load, each classified as claimed, every generation computing the exact reference scores, and zero dropped calls | living_service.rs::the_living_service_story_holds_under_load β four threads hammer score through all six beats |
| An autonomous agent's gate + offline-eval + canary pipeline keeps every bad edit out of production | agent_loop.rs::the_agent_pipeline_keeps_every_bad_edit_out_of_production |
| β The firewall itself β | |
| Body edits re-lower one function while callers are byte-for-byte memo hits; ABI edits cascade | body_edit_does_not_invalidate_callers + signature_edit_does_cascade_to_callers (Arc::ptr_eq on the memo) |
| Function identity is an injective interned id β two names can never collide onto one slot | db.rs::function_ids_are_idempotent_and_collision_free β proven over 5000 names |
Run everything:
cargo test --workspace # 110 tests
cargo run -p blaze-jit --release --example living_service -- --script # the living-service story, asserted
cargo run -p blaze-jit --example agent_loop # the AI-agent safety pipeline, assertedBlaze is a focused core, not a general language. What it deliberately does not do yet β so you know before you build on it:
- One file, one flat namespace. A program is a single
.blazesource of top-level functions. No modules, no imports. - Two scalar types,
int(i64) andfloat(f64). No strings, arrays, structs, or pointers β which is also why a script can't corrupt or fault the host: there are no memory operations to abuse. Explicitintβfloatconversions aren't in the language yet (a mixed-type expression is a proven type error, not a silent coercion). - State lives in the host, not the script. Blaze functions are pure over
their scalar arguments; all persistent state is the embedder's. This is exactly
what makes state survive every reload β but "migrate a script-owned data
structure across a schema change" is therefore not something Blaze does.
StateMigrationis a reservedEditClassfor when script-owned state lands; today it cannot occur. - Retired code is retained, not freed. A concurrent caller may still be inside an old generation mid-swap, so old code pages stay mapped for the runtime's life. The cost is bounded by (edit count Γ edited-function size) β negligible for a dev loop or a human/CI-paced rules service, but it does grow with the number of edits.
- An active canary re-executes each sampled call, so mirror only logic free
of observable host side effects (the norm for scoring/decision functions), and
it adds a lock-free counter to every
call_canarywhile running β noise for a real handler, but real. A canary is an evaluation mode, not steady state. - Watch mode is mtime polling. The file-watching demo re-reads on
modification-time change (the
ScriptHostmechanism); it is not an OS file-event subscription.
None of these change the reload and safety guarantees β growing the surface is roadmap, not architecture. Where a milestone couldn't be made sound in time it was cut and named here, rather than shipped hopeful.
Status & roadmap
- β Incremental firewall (proved by tests since the first commit)
- β Cranelift JIT backend, machine code memoized in the query graph
- β
Live-swap runtime:
Rejected/SafeSwap/Relink/NoEffectclassification, lock-free body swaps, quiesced ABI relinks, host functions, file watching - β Diagnostics gate: syntax errors, undefined callees, arity mismatches, and undefined variables are proven from the query graph and refused before touching a live process β a bad save holds the last-good generation open rather than hot-swapping mangled semantics in
- β
Depth + fuel guards: every function threads a per-call context
(wasmtime's
vmctxpattern). A call-depth counter aborts runaway recursion withErr(ResourceExhausted)before it can blow the native stack, and a fuel budget β one unit per call and per loop back-edge β aborts runaway loops and shallow-but-explosive recursion withErr(FuelExhausted). A single bad edit can no longer fault or wedge the runtime - β
FuncHandlefast path: resolve a function once, then call it lock-free at ~50 M calls/s/thread (scaling linearly across threads). Handles survive body hot-swaps transparently and detect ABI changes without ever dispatching a mismatched call - β
float(f64) + a sound type system:intandfloatare distinct machine representations carried through one raw-64-bit ABI, bit-cast only at call/return boundaries. A type checker in the same query graph refuses every mismatch before a reload commits. Retyping a parameter is an ABI change the firewall classifies as aRelink, atomic even under concurrent calls - β Terminal live demo + scripted CI-safe mode + reload & call benchmarks
- β Per-function metrics: opt-in, lock-free call/latency/fault counters indexed by a function's stable slot (so they survive hot swaps)
- β
Reload journal +
rollback(): every reload event (includingRejectedandNoEffect) is recorded with its class, blast radius, diagnostics, latency, and the exact source it installed.rollback(gen)reinstalls that generation's source through the ordinary reload protocol - β
In-process canary:
canary(source, policy)compiles an isolated candidate and shadows it against the live program; the caller always gets the live answer. A wrong, faulting, or too-slow candidate auto-aborts per policy and cannot be promoted. The 1-in-N sampler is lock-free - β
The demo β a living service under fire:
examples/living_serviceis an HTTP scoring service whose logic is a hot-swapped.blazeprogram, with a live TUI. Its--scriptmode runs the six-beat story over real HTTP under load, every beat asserted, zero dropped.examples/agent_loopruns the same gate β canary β promote/abort pipeline for an autonomous editor - β
Benchmark suite: reload latency per class, call throughput (named vs
handle), Blaze vs. an embedded interpreter (
rhai, ~51Γ), and canary overhead - π Richer types and explicit
intβfloatconversions (pure language growth; reload semantics unchanged) - π
StateMigration: script-owned persistent state with layout versioning - π Windowed demo (the terminal demos are engine-complete; a
winit/macroquadfrontend is presentation) and an editor status-line plugin
Mechanics β the swap table, the compiler, the language (one paragraph each)
The swap table. Every function gets a slot in an mmap'd array of atomic
pointers with a process-stable address. Every BlazeβBlaze call compiles to an
acquire-load of the callee's slot plus an indirect call β so functions have no
relocations against each other, generations of code never need relinking, and
"swap" is one release-store. Executable pages are managed by Cranelift's JIT;
retired generations stay mapped for the runtime's life because a concurrent
caller may still be inside them β the cost is old versions of edited functions
only.
The compiler. blaze-parse lexes with logos and parses with a
hand-written recursive-descent parser into a lossless rowan CST. blaze-ir
lowers to DevIR β a tiny register IR with label-structured control flow β
inside the salsa graph. blaze-jit maps DevIR onto Cranelift (registers
become cranelift-frontend variables; the SSA builder reconstructs phis) and
emits through one shared pass with pluggable call emission (direct, relocatable,
or table-indirect).
The language. A deliberately small C-subset, JIT-compiled to native code:
int (i64) and float (IEEE-754 f64) functions, parameters, locals,
assignment, + - * /, comparisons, if / else if / else, while, recursion,
calls, unary minus. The two scalar types are distinct and do not implicitly
convert; a mixed-type expression, a wrong-typed return or assignment, or a
bare-float condition is a type error the same query graph proves and
Rejecteds before any code goes live. Nothing a script can express β not a
saved typo, not a divide-by-zero, not runaway recursion, not an infinite loop β
can fault or hang the process embedding it.
Design notes
- Modern salsa. Blaze targets salsa 0.28 (
#[salsa::tracked]functions over a#[salsa::db]trait) β the maintained API that rust-analyzer uses, not the long-removedquery_groupmacros. - Function identity is interned, not hashed. A function's id is salsa's interned id for its name β an injective map β so two distinct names can never collide onto one id and route a call to the wrong function.
- Generations are retired, not freed. Deliberate: soundness under concurrent callers first; the leak is bounded by edit count Γ edited-function size, negligible for a dev loop.
- Host-facing arity is part of your API.
reloadkeeps hostcalls safe (ArityMismatcherrors rather than UB) if you change an entrypoint's signature while the host still passes the old argument count.
Apache-2.0 Β·
grloper.github.io/Blaze Β·
cargo test --workspace # 110 tests