update
This commit is contained in:
@@ -1,53 +1,52 @@
|
||||
//! `runtime_under_test` — the runtime that CI proves equivalent to the
|
||||
//! reference. It is configuration-driven: the canonical configuration must
|
||||
//! match the reference bit-for-bit, while semantic mutation swaps in a mutated
|
||||
//! configuration to verify the test suite can detect any divergence.
|
||||
//!
|
||||
//! Per the spec's mandatory order, the *optimized* runtime may not begin until
|
||||
//! steps 1–7 pass CI; until then this runtime is the reference engine driven
|
||||
//! through the same config surface, which is by construction equivalent.
|
||||
//! reference. Unlike the reference, this crate does **not** call the reference
|
||||
//! engine: it carries its own independent interpreter ([`native::native_resolve`])
|
||||
//! re-derived from the spec. The runtime-equivalence gate therefore compares
|
||||
//! two genuinely separate implementations, so 100% agreement is *evidence* that
|
||||
//! the spec is implemented correctly rather than a tautology. A transcription
|
||||
//! error in either implementation surfaces as an equivalence failure (proven by
|
||||
//! the negative-control test below).
|
||||
|
||||
use reference_runtime::{execute, EngineConfig, ResolutionInput, ResolutionResult, Runtime};
|
||||
pub mod native;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RuntimeUnderTest {
|
||||
pub config: EngineConfig,
|
||||
}
|
||||
use reference_runtime::{ResolutionInput, ResolutionResult, Runtime};
|
||||
|
||||
impl Default for RuntimeUnderTest {
|
||||
fn default() -> Self {
|
||||
RuntimeUnderTest {
|
||||
config: EngineConfig::reference(),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use native::native_resolve;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct RuntimeUnderTest;
|
||||
|
||||
impl RuntimeUnderTest {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Construct with a specific engine config (used by semantic mutation to
|
||||
/// install a mutated artifact).
|
||||
pub fn with_config(config: EngineConfig) -> Self {
|
||||
RuntimeUnderTest { config }
|
||||
RuntimeUnderTest
|
||||
}
|
||||
}
|
||||
|
||||
impl Runtime for RuntimeUnderTest {
|
||||
fn resolve(&self, input: ResolutionInput) -> ResolutionResult {
|
||||
execute(&self.config, &input)
|
||||
native_resolve(&input)
|
||||
}
|
||||
}
|
||||
|
||||
/// A deliberately broken runtime used as a negative control: it shares the
|
||||
/// independent interpreter but corrupts one recorded value. The equivalence
|
||||
/// gate **must** reject it. This proves the gate can fail.
|
||||
#[cfg(any(test, feature = "negative_controls"))]
|
||||
pub fn buggy_resolve(input: &ResolutionInput) -> ResolutionResult {
|
||||
let mut r = native_resolve(input);
|
||||
// Drop a single causal edge — a subtle bug an honest gate has to catch.
|
||||
r.trace.causal_graph.edges.pop();
|
||||
r
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use reference_runtime::{canonical, execute, ReferenceRuntime};
|
||||
use rune_ir::{Op, RuneProgram, RuneToken};
|
||||
use reference_runtime::{canonical, execute, EngineConfig};
|
||||
use rune_ir::{Op, RuneProgram, RuneToken, ALL_OPS};
|
||||
use world_model::{standard_executors, ProgramId, Rng, WorldId, WorldSnapshot, NUM_DOMAINS};
|
||||
|
||||
fn random_input(seed: u64) -> ResolutionInput {
|
||||
fn rich_input(seed: u64) -> ResolutionInput {
|
||||
let mut rng = Rng::new(seed);
|
||||
let mut w = WorldSnapshot::blank(WorldId(seed), seed);
|
||||
for d in &mut w.domains {
|
||||
@@ -63,9 +62,9 @@ mod tests {
|
||||
w.causal_state.coupling[j][i] = rng.range_i64(-17, 17);
|
||||
}
|
||||
}
|
||||
let tokens: Vec<RuneToken> = (0..30)
|
||||
.map(|_| RuneToken {
|
||||
op: Op::from_u8(rng.next_u64() as u8),
|
||||
let tokens: Vec<RuneToken> = (0..40)
|
||||
.map(|i| RuneToken {
|
||||
op: if i % 3 == 0 { ALL_OPS[i % 12] } else { Op::from_u8(rng.next_u64() as u8) },
|
||||
a: rng.next_u64() as u8,
|
||||
b: rng.next_u64() as u8,
|
||||
c: rng.next_u64() as u8,
|
||||
@@ -75,25 +74,40 @@ mod tests {
|
||||
ResolutionInput {
|
||||
world: w,
|
||||
program: RuneProgram { id: ProgramId(seed), tokens, seed },
|
||||
contexts: standard_executors(seed, 3),
|
||||
contexts: standard_executors(seed, 4),
|
||||
contract_seed: seed,
|
||||
perturbation_seed: seed,
|
||||
}
|
||||
}
|
||||
|
||||
/// The core honesty property: the independent interpreter reproduces the
|
||||
/// reference engine bit-for-bit over a large seed sweep. This is what makes
|
||||
/// the equivalence gate meaningful rather than vacuous.
|
||||
#[test]
|
||||
fn rut_matches_reference() {
|
||||
use reference_runtime::Runtime;
|
||||
let rut = RuntimeUnderTest::new();
|
||||
let reference = ReferenceRuntime::new();
|
||||
for s in 0..200 {
|
||||
let input = random_input(s);
|
||||
let a = canonical(&reference.resolve(input.clone()));
|
||||
let b = canonical(&rut.resolve(input.clone()));
|
||||
assert_eq!(a, b, "divergence at seed {s}");
|
||||
// and against the raw engine path
|
||||
let c = canonical(&execute(&EngineConfig::reference(), &input));
|
||||
assert_eq!(a, c);
|
||||
fn native_matches_reference_bit_for_bit() {
|
||||
let cfg = EngineConfig::reference();
|
||||
for s in 0..2000u64 {
|
||||
let input = rich_input(s.wrapping_mul(0x9e3779b97f4a7c15) ^ 0xabc);
|
||||
let a = canonical(&execute(&cfg, &input));
|
||||
let b = canonical(&native_resolve(&input));
|
||||
assert_eq!(a, b, "independent interpreter diverged at seed {s}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Negative control: a runtime with a real bug is rejected by the canonical
|
||||
/// comparison. Proves the equivalence gate is not vacuous.
|
||||
#[test]
|
||||
fn buggy_runtime_is_rejected() {
|
||||
let cfg = EngineConfig::reference();
|
||||
let mut caught = 0;
|
||||
for s in 0..200u64 {
|
||||
let input = rich_input(s + 1);
|
||||
let a = canonical(&execute(&cfg, &input));
|
||||
let b = canonical(&buggy_resolve(&input));
|
||||
if a != b {
|
||||
caught += 1;
|
||||
}
|
||||
}
|
||||
assert!(caught > 0, "the equivalence gate failed to catch a buggy runtime");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,503 @@
|
||||
//! An *independent* interpreter for the canonical engine behavior.
|
||||
//!
|
||||
//! This is the whole point of the runtime-equivalence gate: if the runtime
|
||||
//! under test merely called `reference_runtime::execute`, agreement would be a
|
||||
//! tautology and a bug in the shared interpreter would hide in both. This file
|
||||
//! re-derives the executable spec's canonical behavior from scratch, in a
|
||||
//! different code organization (a register-machine `Vm` rather than the
|
||||
//! reference's free-function dispatch), depending only on the shared *data*
|
||||
//! crates (`world_model`, `trace_model`, `rune_ir`) and never on the
|
||||
//! reference's engine. When the two implementations agree it is evidence; when
|
||||
//! a transcription error is introduced, the equivalence gate catches it (see
|
||||
//! the negative-control tests).
|
||||
//!
|
||||
//! Because the runtime under test only ever needs to reproduce the *canonical*
|
||||
//! reference configuration, the engine constants are inlined here as literals —
|
||||
//! they are the spec, independently restated, not imported.
|
||||
|
||||
use reference_runtime::{ResolutionInput, ResolutionResult};
|
||||
use rune_ir::{Op, RuneProgram, RuneToken};
|
||||
use trace_model::{
|
||||
BehaviorFingerprint, CausalEdge, CausalGraph, CausalNode, DivergenceGraph, DomainAccessGraph,
|
||||
ExecutionTrace, FaultCode, FaultLog, InformationFlowGraph, PerturbationResponse, ReplayRecord,
|
||||
TemporalGraph,
|
||||
};
|
||||
use world_model::{
|
||||
DomainKind, ExecutionContext, Hash, Hasher, ScheduledEffect, WorldDelta, WorldSnapshot,
|
||||
DomainId, HIDDEN_LANES, LANES, NUM_DOMAINS, REGS,
|
||||
};
|
||||
|
||||
// --- The canonical engine constants, independently restated. ---------------
|
||||
const C1: u64 = 0xff51afd7ed558ccd;
|
||||
const C2: u64 = 0xc4ceb9fe1a85ec53;
|
||||
const S1: u32 = 33;
|
||||
const S2: u32 = 29;
|
||||
const S3: u32 = 32;
|
||||
const FUTURE_TURNS: usize = 3;
|
||||
const DIFFUSE_SPAN: usize = 3;
|
||||
|
||||
#[inline]
|
||||
fn avalanche(z: i64) -> i64 {
|
||||
let mut u = z as u64;
|
||||
u ^= u >> S1;
|
||||
u = u.wrapping_mul(C1);
|
||||
u ^= u >> S2;
|
||||
u = u.wrapping_mul(C2);
|
||||
u ^= u >> S3;
|
||||
u as i64
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn combine(ctx: &ExecutionContext, a: i64, b: i64, coupling: i64, kc: u64) -> i64 {
|
||||
let mut z = a.wrapping_mul(kc as i64);
|
||||
z ^= b.rotate_left(((kc & 31) as u32) + 1);
|
||||
z = z.wrapping_add(coupling.wrapping_mul(b & 0xffff));
|
||||
z ^= ctx.salt() as i64;
|
||||
z = z.wrapping_add(ctx.profile.bias);
|
||||
z = z.rotate_left((ctx.profile.rotate % 63) + 1);
|
||||
avalanche(z)
|
||||
}
|
||||
|
||||
/// A register-machine view of one single-executor run. Holds the working world,
|
||||
/// the trace graphs accumulated as the program executes, and the accumulator
|
||||
/// file. Organised as a stateful object with `&mut self` methods, deliberately
|
||||
/// unlike the reference's stateless free functions.
|
||||
struct Vm<'a> {
|
||||
w: WorldSnapshot,
|
||||
ctx: &'a ExecutionContext,
|
||||
read_graph: DomainAccessGraph,
|
||||
write_graph: DomainAccessGraph,
|
||||
causal_graph: CausalGraph,
|
||||
info_flow: InformationFlowGraph,
|
||||
temporal: TemporalGraph,
|
||||
faults: FaultLog,
|
||||
acc: [i64; REGS],
|
||||
acc_src: [usize; REGS],
|
||||
}
|
||||
|
||||
impl<'a> Vm<'a> {
|
||||
fn new(world: &WorldSnapshot, ctx: &'a ExecutionContext) -> Self {
|
||||
let acc = world.execution_state.accumulator;
|
||||
Vm {
|
||||
w: world.clone(),
|
||||
ctx,
|
||||
read_graph: DomainAccessGraph::default(),
|
||||
write_graph: DomainAccessGraph::default(),
|
||||
causal_graph: CausalGraph::default(),
|
||||
info_flow: InformationFlowGraph::default(),
|
||||
temporal: TemporalGraph::default(),
|
||||
faults: FaultLog::default(),
|
||||
acc,
|
||||
acc_src: [0; REGS],
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn rd(&self, dom: usize, lane: usize, hidden: bool) -> i64 {
|
||||
if hidden {
|
||||
self.w.domains[dom].hidden[lane % HIDDEN_LANES]
|
||||
} else {
|
||||
self.w.domains[dom].observed[lane % LANES]
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn wr(&mut self, dom: usize, lane: usize, hidden: bool, val: i64) {
|
||||
if hidden {
|
||||
self.w.domains[dom].hidden[lane % HIDDEN_LANES] = val;
|
||||
} else {
|
||||
self.w.domains[dom].observed[lane % LANES] = val;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn coupling(&self, to: usize, from: usize) -> i64 {
|
||||
self.w.causal_state.coupling[to][from]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn mix(&self, a: i64, b: i64, coupling: i64, kc: u64) -> i64 {
|
||||
combine(self.ctx, a, b, coupling, kc)
|
||||
}
|
||||
|
||||
/// Record one data-movement edge across every graph, in the canonical order.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn flow(
|
||||
&mut self,
|
||||
from_dom: usize,
|
||||
from_lane: usize,
|
||||
from_hidden: bool,
|
||||
to_dom: usize,
|
||||
to_lane: usize,
|
||||
to_hidden: bool,
|
||||
step: u32,
|
||||
weight: i64,
|
||||
) {
|
||||
self.read_graph.access_count[from_dom] += 1;
|
||||
self.write_graph.access_count[to_dom] += 1;
|
||||
self.read_graph.edges.push((from_dom as u8, to_dom as u8, 1));
|
||||
self.write_graph.edges.push((from_dom as u8, to_dom as u8, 1));
|
||||
self.info_flow
|
||||
.edges
|
||||
.push((from_dom as u8, to_dom as u8, (weight as u64).count_ones()));
|
||||
self.causal_graph.edges.push(CausalEdge {
|
||||
from: CausalNode {
|
||||
domain: from_dom as u8,
|
||||
lane: from_lane as u8,
|
||||
hidden: from_hidden,
|
||||
step,
|
||||
},
|
||||
to: CausalNode {
|
||||
domain: to_dom as u8,
|
||||
lane: to_lane as u8,
|
||||
hidden: to_hidden,
|
||||
step,
|
||||
},
|
||||
weight,
|
||||
});
|
||||
}
|
||||
|
||||
fn run(&mut self, program: &RuneProgram) {
|
||||
for (i, tok) in program.tokens.iter().enumerate() {
|
||||
let step = i as u32;
|
||||
self.step(tok, step);
|
||||
let dst = tok.dst_domain();
|
||||
let lane = tok.lane();
|
||||
let r = (step as usize) % REGS;
|
||||
self.acc[r] = self.acc[r].wrapping_add(self.w.domains[dst].observed[lane]);
|
||||
}
|
||||
self.w.execution_state.accumulator = self.acc;
|
||||
}
|
||||
|
||||
fn step(&mut self, tok: &RuneToken, step: u32) {
|
||||
let src = tok.src_domain();
|
||||
let dst = tok.dst_domain();
|
||||
let lane = tok.lane();
|
||||
let lane2 = tok.lane2();
|
||||
let kc = DomainKind::from_index(dst).mix_const();
|
||||
let coupling = self.coupling(dst, src);
|
||||
|
||||
match tok.op {
|
||||
Op::Mix => {
|
||||
let a = self.rd(src, lane, false);
|
||||
let b = self.rd(dst, lane2, false);
|
||||
let v = self.mix(a, b, coupling, kc);
|
||||
self.wr(dst, lane, false, v);
|
||||
self.flow(src, lane, false, dst, lane, false, step, v);
|
||||
self.flow(dst, lane2, false, dst, lane, false, step, v);
|
||||
}
|
||||
Op::Channel => {
|
||||
let a = self.rd(src, lane, false);
|
||||
let v = self.mix(a, coupling, coupling, kc);
|
||||
self.wr(dst, lane2, false, v);
|
||||
self.flow(src, lane, false, dst, lane2, false, step, v);
|
||||
}
|
||||
Op::Branch => {
|
||||
let probe = self.rd(src, lane, false);
|
||||
let take_hot =
|
||||
probe.wrapping_add(self.ctx.profile.bias) > self.ctx.profile.branch_threshold;
|
||||
if take_hot {
|
||||
let b = self.rd(dst, lane, false);
|
||||
let v = self.mix(probe, b, coupling, kc);
|
||||
self.wr(dst, lane, false, v);
|
||||
self.flow(src, lane, false, dst, lane, false, step, v);
|
||||
} else {
|
||||
let b = self.rd(dst, lane2, false);
|
||||
let v = self.mix(b, probe, coupling, kc).wrapping_add(0x5bd1e9);
|
||||
self.wr(dst, lane2, false, v);
|
||||
self.flow(src, lane, false, dst, lane2, false, step, v);
|
||||
self.faults.push(FaultCode::UnreachableBranch, step, 0);
|
||||
}
|
||||
}
|
||||
Op::Schedule => {
|
||||
let a = self.rd(src, lane, false);
|
||||
let b = self.rd(dst, lane, false);
|
||||
let v = self.mix(a, b, coupling, kc);
|
||||
let offset = 1 + (tok.imm.rem_euclid(3)) as u8;
|
||||
let hidden = tok.mode() & 1 == 1;
|
||||
self.w.time_state.pending.push(ScheduledEffect {
|
||||
turn_offset: offset,
|
||||
domain: DomainId(dst as u8),
|
||||
lane,
|
||||
hidden,
|
||||
value: v,
|
||||
});
|
||||
self.temporal.edges.push((step, offset, dst as u8));
|
||||
self.flow(src, lane, false, dst, lane, hidden, step, v);
|
||||
}
|
||||
Op::Resonate => {
|
||||
let a = self.rd(src, lane, false);
|
||||
let b = self.rd(dst, lane, false);
|
||||
let m = self.mix(a, b, coupling, kc);
|
||||
let va = a.wrapping_add(m);
|
||||
let vb = b ^ m;
|
||||
self.wr(src, lane, false, va);
|
||||
self.wr(dst, lane, false, vb);
|
||||
self.flow(dst, lane, false, src, lane, false, step, va);
|
||||
self.flow(src, lane, false, dst, lane, false, step, vb);
|
||||
}
|
||||
Op::Observe => {
|
||||
let reg = tok.mode() % REGS;
|
||||
let mut z: i64 = self.acc[reg];
|
||||
let proj = self.w.observed_projection();
|
||||
for k in 0..3 {
|
||||
let d = (src + k) % NUM_DOMAINS;
|
||||
let idx = d * LANES + (lane + k) % LANES;
|
||||
let cpl = self.coupling(dst, d);
|
||||
z = self.mix(z, proj[idx], cpl, kc);
|
||||
self.flow(d, (lane + k) % LANES, false, dst, lane, true, step, z);
|
||||
}
|
||||
self.acc[reg] = z;
|
||||
self.acc_src[reg] = src;
|
||||
self.wr(dst, tok.mode() % HIDDEN_LANES, true, z);
|
||||
}
|
||||
Op::Collapse => {
|
||||
let reg = tok.mode() % REGS;
|
||||
let a = self.acc[reg];
|
||||
let b = self.rd(dst, lane, false);
|
||||
if a == 0 {
|
||||
self.faults.push(FaultCode::EmptyAccumulator, step, reg as i64);
|
||||
}
|
||||
let v = self.mix(a, b, coupling, kc);
|
||||
self.wr(dst, lane, false, v);
|
||||
let asrc = self.acc_src[reg];
|
||||
self.flow(asrc, 0, true, dst, lane, false, step, v);
|
||||
}
|
||||
Op::Invert => {
|
||||
let b = self.rd(dst, lane, false);
|
||||
let mut v = avalanche((!b).wrapping_add(tok.imm));
|
||||
v ^= self.ctx.salt() as i64;
|
||||
v = v.wrapping_add(self.ctx.profile.bias);
|
||||
self.wr(dst, lane, false, v);
|
||||
self.flow(dst, lane, false, dst, lane, false, step, v);
|
||||
}
|
||||
Op::Diffuse => {
|
||||
let a = self.rd(src, lane, false);
|
||||
for k in 0..DIFFUSE_SPAN {
|
||||
let d = (src + 1 + k) % NUM_DOMAINS;
|
||||
let tl = (lane + k) % LANES;
|
||||
let prev = self.rd(d, tl, false);
|
||||
let cpl = self.coupling(d, src);
|
||||
let kc2 = DomainKind::from_index(d).mix_const();
|
||||
let v = self.mix(a, prev, cpl, kc2);
|
||||
self.wr(d, tl, false, prev.wrapping_add(v));
|
||||
self.flow(src, lane, false, d, tl, false, step, v);
|
||||
}
|
||||
}
|
||||
Op::Anchor => {
|
||||
let bound = (tok.imm.unsigned_abs() % 1_000_000) as i64 + 1;
|
||||
let b = self.rd(dst, lane, false);
|
||||
let diag = self.coupling(dst, dst);
|
||||
let mut mixed = b.wrapping_add(diag);
|
||||
mixed = mixed
|
||||
.wrapping_add(self.ctx.profile.bias)
|
||||
.wrapping_add((self.ctx.salt() & 0xffff) as i64);
|
||||
let clamped = mixed.clamp(-bound, bound);
|
||||
if clamped != mixed {
|
||||
self.faults.push(FaultCode::Saturated, step, bound);
|
||||
}
|
||||
self.wr(dst, lane, false, clamped);
|
||||
self.flow(dst, lane, false, dst, lane, false, step, clamped);
|
||||
}
|
||||
Op::Echoback => {
|
||||
let h = self.rd(dst, tok.mode() % HIDDEN_LANES, true);
|
||||
let b = self.rd(dst, lane, false);
|
||||
let v = self.mix(h, b, coupling, kc);
|
||||
self.wr(dst, lane, false, v);
|
||||
self.flow(dst, tok.mode() % HIDDEN_LANES, true, dst, lane, false, step, v);
|
||||
}
|
||||
Op::Imprint => {
|
||||
let b = self.rd(dst, lane, false);
|
||||
let hl = tok.mode() % HIDDEN_LANES;
|
||||
let prevh = self.rd(dst, hl, true);
|
||||
let v = self.mix(b, prevh, coupling, kc);
|
||||
self.wr(dst, hl, true, v);
|
||||
self.flow(dst, lane, false, dst, hl, true, step, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Advance the world one turn: resolve due scheduled effects, then run coupling
|
||||
/// diffusion. Identical semantics to the reference's `step_world`, restated.
|
||||
fn step_world(w: &mut WorldSnapshot) {
|
||||
let pending = std::mem::take(&mut w.time_state.pending);
|
||||
let mut still = Vec::new();
|
||||
for e in pending {
|
||||
if e.turn_offset <= 1 {
|
||||
let d = e.domain.0 as usize;
|
||||
if e.hidden {
|
||||
let l = e.lane % HIDDEN_LANES;
|
||||
w.domains[d].hidden[l] = w.domains[d].hidden[l].wrapping_add(e.value);
|
||||
} else {
|
||||
let l = e.lane % LANES;
|
||||
w.domains[d].observed[l] = w.domains[d].observed[l].wrapping_add(e.value);
|
||||
}
|
||||
} else {
|
||||
still.push(ScheduledEffect {
|
||||
turn_offset: e.turn_offset - 1,
|
||||
..e
|
||||
});
|
||||
}
|
||||
}
|
||||
w.time_state.pending = still;
|
||||
|
||||
let snap = w.domains.clone();
|
||||
for j in 0..NUM_DOMAINS {
|
||||
for lane in 0..LANES {
|
||||
let mut z = w.domains[j].observed[lane];
|
||||
for i in 0..NUM_DOMAINS {
|
||||
let c = w.causal_state.coupling[j][i];
|
||||
z = z.wrapping_add(c.wrapping_mul(snap[i].observed[lane] & 0xff));
|
||||
}
|
||||
w.domains[j].observed[lane] = avalanche(z);
|
||||
}
|
||||
for hl in 0..HIDDEN_LANES {
|
||||
let base = w.domains[j].hidden[hl].wrapping_add(snap[j].observed[0]);
|
||||
w.domains[j].hidden[hl] = avalanche(base);
|
||||
}
|
||||
}
|
||||
w.turn = w.turn.wrapping_add(1);
|
||||
}
|
||||
|
||||
fn future_hash(start: &WorldSnapshot) -> Hash {
|
||||
let mut w = start.clone();
|
||||
let mut h = Hasher::new();
|
||||
h.write_tag("future-3");
|
||||
for _ in 0..FUTURE_TURNS {
|
||||
step_world(&mut w);
|
||||
for v in w.ground_truth() {
|
||||
h.write_i64(v);
|
||||
}
|
||||
}
|
||||
h.finish()
|
||||
}
|
||||
|
||||
fn compute_divergence(finals: &[WorldSnapshot]) -> DivergenceGraph {
|
||||
let n = finals.len();
|
||||
let mut pairwise = vec![0.0f64; n * n];
|
||||
let total = (NUM_DOMAINS * LANES) as f64;
|
||||
for i in 0..n {
|
||||
for j in 0..n {
|
||||
if i == j {
|
||||
continue;
|
||||
}
|
||||
let mut diff = 0usize;
|
||||
for d in 0..NUM_DOMAINS {
|
||||
for l in 0..LANES {
|
||||
if finals[i].domains[d].observed[l] != finals[j].domains[d].observed[l] {
|
||||
diff += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
pairwise[i * n + j] = diff as f64 / total;
|
||||
}
|
||||
}
|
||||
DivergenceGraph {
|
||||
executor_count: n,
|
||||
pairwise,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn behavior_fingerprint(
|
||||
delta: &WorldDelta,
|
||||
causal: &CausalGraph,
|
||||
read_graph: &DomainAccessGraph,
|
||||
write_graph: &DomainAccessGraph,
|
||||
info_flow: &InformationFlowGraph,
|
||||
temporal: &TemporalGraph,
|
||||
divergence: &DivergenceGraph,
|
||||
future: Hash,
|
||||
) -> BehaviorFingerprint {
|
||||
let mut features: Vec<i64> = Vec::new();
|
||||
for dd in &delta.domain_deltas {
|
||||
let mut s = 0i64;
|
||||
for &v in &dd.observed {
|
||||
s = s.wrapping_add(v);
|
||||
}
|
||||
features.push(s);
|
||||
}
|
||||
for dd in &delta.domain_deltas {
|
||||
let mut s = 0i64;
|
||||
for &v in &dd.hidden {
|
||||
s = s.wrapping_add(v);
|
||||
}
|
||||
features.push(s);
|
||||
}
|
||||
features.push(causal.causal_rank() as i64);
|
||||
features.push(causal.edge_count() as i64);
|
||||
features.push(read_graph.touched_count() as i64);
|
||||
features.push(write_graph.touched_count() as i64);
|
||||
features.push(info_flow.total_bits() as i64);
|
||||
features.push(temporal.edge_count() as i64);
|
||||
features.push((divergence.mean_divergence() * 1_000_000.0) as i64);
|
||||
features.push(future.0 as i64);
|
||||
BehaviorFingerprint::from_features(features)
|
||||
}
|
||||
|
||||
/// The independent implementation of the canonical resolution.
|
||||
pub fn native_resolve(input: &ResolutionInput) -> ResolutionResult {
|
||||
let contexts = if input.contexts.is_empty() {
|
||||
world_model::standard_executors(input.world.seed, 3)
|
||||
} else {
|
||||
input.contexts.clone()
|
||||
};
|
||||
|
||||
let world0 = input.world.clone();
|
||||
|
||||
let mut finals: Vec<WorldSnapshot> = Vec::with_capacity(contexts.len());
|
||||
let mut primary: Option<Vm> = None;
|
||||
for (idx, ctx) in contexts.iter().enumerate() {
|
||||
let mut vm = Vm::new(&world0, ctx);
|
||||
vm.run(&input.program);
|
||||
finals.push(vm.w.clone());
|
||||
if idx == 0 {
|
||||
primary = Some(vm);
|
||||
}
|
||||
}
|
||||
let vm = primary.expect("at least one executor");
|
||||
|
||||
let delta = WorldDelta::between(&world0, &vm.w);
|
||||
let divergence = compute_divergence(&finals);
|
||||
let fhash = future_hash(&vm.w);
|
||||
let behavior = behavior_fingerprint(
|
||||
&delta,
|
||||
&vm.causal_graph,
|
||||
&vm.read_graph,
|
||||
&vm.write_graph,
|
||||
&vm.info_flow,
|
||||
&vm.temporal,
|
||||
&divergence,
|
||||
fhash,
|
||||
);
|
||||
|
||||
let trace = ExecutionTrace {
|
||||
read_graph: vm.read_graph,
|
||||
write_graph: vm.write_graph,
|
||||
causal_graph: vm.causal_graph,
|
||||
information_flow: vm.info_flow,
|
||||
executor_divergence: divergence,
|
||||
temporal_graph: vm.temporal,
|
||||
perturbation_response: PerturbationResponse::default(),
|
||||
behavior_fingerprint: behavior,
|
||||
};
|
||||
let trace_hash = trace.canonical_hash();
|
||||
let delta_hash = delta.hash();
|
||||
let replay = ReplayRecord {
|
||||
world_seed: input.world.seed,
|
||||
program_seed: input.program.seed,
|
||||
contract_seed: input.contract_seed,
|
||||
perturbation_seed: input.perturbation_seed,
|
||||
trace_hash,
|
||||
delta_hash,
|
||||
future_hash: fhash,
|
||||
};
|
||||
|
||||
ResolutionResult {
|
||||
delta,
|
||||
trace,
|
||||
faults: vm.faults,
|
||||
replay,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user