changes claude never committed
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "world_model"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
@@ -0,0 +1,107 @@
|
||||
//! Execution contexts (executors). Different executors interpret the same
|
||||
//! rune stream differently, which produces executor divergence. The spec
|
||||
//! requires at least 3 distinct executors per case.
|
||||
|
||||
use crate::primitives::{Hasher, Rng};
|
||||
|
||||
/// Distinct executor interpretation styles. Each one mixes rune operands
|
||||
/// differently, so the same program produces different traces under each.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum ExecutorKind {
|
||||
/// Aggressive forward mixing; favors multiplicative coupling.
|
||||
Surge,
|
||||
/// Lateral mixing; favors xor/rotate coupling across domains.
|
||||
Weave,
|
||||
/// Conservative mixing; clamps and favors additive coupling.
|
||||
Anchor,
|
||||
/// Phase-shifting; reorders operand roles.
|
||||
Phase,
|
||||
}
|
||||
|
||||
pub const ALL_EXECUTOR_KINDS: [ExecutorKind; 4] = [
|
||||
ExecutorKind::Surge,
|
||||
ExecutorKind::Weave,
|
||||
ExecutorKind::Anchor,
|
||||
ExecutorKind::Phase,
|
||||
];
|
||||
|
||||
impl ExecutorKind {
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
ExecutorKind::Surge => "surge",
|
||||
ExecutorKind::Weave => "weave",
|
||||
ExecutorKind::Anchor => "anchor",
|
||||
ExecutorKind::Phase => "phase",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn index(self) -> usize {
|
||||
match self {
|
||||
ExecutorKind::Surge => 0,
|
||||
ExecutorKind::Weave => 1,
|
||||
ExecutorKind::Anchor => 2,
|
||||
ExecutorKind::Phase => 3,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn salt(self) -> u64 {
|
||||
match self {
|
||||
ExecutorKind::Surge => 0x51_75_72_67_65_00_00_01,
|
||||
ExecutorKind::Weave => 0x57_65_61_76_65_00_00_02,
|
||||
ExecutorKind::Anchor => 0x41_6e_63_68_72_00_00_03,
|
||||
ExecutorKind::Phase => 0x50_68_61_73_65_00_00_04,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters that modulate rune interpretation for one executor.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub struct ExecutorProfile {
|
||||
pub kind: ExecutorKind,
|
||||
pub bias: i64,
|
||||
pub rotate: u32,
|
||||
pub branch_threshold: i64,
|
||||
}
|
||||
|
||||
/// One executor.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub struct ExecutionContext {
|
||||
pub executor_id: u32,
|
||||
pub profile: ExecutorProfile,
|
||||
}
|
||||
|
||||
impl ExecutionContext {
|
||||
pub fn hash_into(&self, h: &mut Hasher) {
|
||||
h.write_tag("executor");
|
||||
h.write_u64(self.executor_id as u64);
|
||||
h.write_u8(self.profile.kind.index() as u8);
|
||||
h.write_i64(self.profile.bias);
|
||||
h.write_u64(self.profile.rotate as u64);
|
||||
h.write_i64(self.profile.branch_threshold);
|
||||
}
|
||||
|
||||
pub fn salt(&self) -> u64 {
|
||||
self.profile.kind.salt() ^ (self.executor_id as u64).wrapping_mul(0x9e3779b97f4a7c15)
|
||||
}
|
||||
}
|
||||
|
||||
/// Build `count` distinct executors deterministically from a seed. Always
|
||||
/// produces at least 3 with distinct kinds.
|
||||
pub fn standard_executors(seed: u64, count: usize) -> Vec<ExecutionContext> {
|
||||
let count = count.max(3);
|
||||
let mut rng = Rng::derive(seed, "executors");
|
||||
let mut out = Vec::with_capacity(count);
|
||||
for i in 0..count {
|
||||
let kind = ALL_EXECUTOR_KINDS[i % ALL_EXECUTOR_KINDS.len()];
|
||||
out.push(ExecutionContext {
|
||||
executor_id: i as u32,
|
||||
profile: ExecutorProfile {
|
||||
kind,
|
||||
bias: rng.range_i64(-7, 7),
|
||||
rotate: (1 + rng.below(31)) as u32,
|
||||
branch_threshold: rng.range_i64(-1000, 1000),
|
||||
},
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
//! World domains. The spec mandates at least 8 *independent* domains, each
|
||||
//! exposing read/write surfaces, perturbation axes, and a fingerprint.
|
||||
//!
|
||||
//! Each domain holds `LANES` observed values and `HIDDEN_LANES` hidden values.
|
||||
//! Domains differ from one another by per-kind mixing constants and by the
|
||||
//! perturbation axes they expose, which is what makes them genuinely
|
||||
//! independent rather than eight copies of one decorative axis.
|
||||
|
||||
use crate::perturb::{
|
||||
HiddenFlipAxis, LaneBumpAxis, LaneScaleAxis, LaneSwapAxis, PerturbationAxis,
|
||||
};
|
||||
use crate::primitives::{DomainId, Hash, Hasher, HIDDEN_LANES, LANES, NUM_DOMAINS};
|
||||
|
||||
/// The eight independent domains.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
|
||||
pub enum DomainKind {
|
||||
Aether,
|
||||
Matter,
|
||||
Flux,
|
||||
Mind,
|
||||
Entropy,
|
||||
Resonance,
|
||||
Boundary,
|
||||
Echo,
|
||||
}
|
||||
|
||||
pub const ALL_DOMAIN_KINDS: [DomainKind; NUM_DOMAINS] = [
|
||||
DomainKind::Aether,
|
||||
DomainKind::Matter,
|
||||
DomainKind::Flux,
|
||||
DomainKind::Mind,
|
||||
DomainKind::Entropy,
|
||||
DomainKind::Resonance,
|
||||
DomainKind::Boundary,
|
||||
DomainKind::Echo,
|
||||
];
|
||||
|
||||
impl DomainKind {
|
||||
pub fn index(self) -> usize {
|
||||
match self {
|
||||
DomainKind::Aether => 0,
|
||||
DomainKind::Matter => 1,
|
||||
DomainKind::Flux => 2,
|
||||
DomainKind::Mind => 3,
|
||||
DomainKind::Entropy => 4,
|
||||
DomainKind::Resonance => 5,
|
||||
DomainKind::Boundary => 6,
|
||||
DomainKind::Echo => 7,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_index(i: usize) -> DomainKind {
|
||||
ALL_DOMAIN_KINDS[i % NUM_DOMAINS]
|
||||
}
|
||||
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
DomainKind::Aether => "aether",
|
||||
DomainKind::Matter => "matter",
|
||||
DomainKind::Flux => "flux",
|
||||
DomainKind::Mind => "mind",
|
||||
DomainKind::Entropy => "entropy",
|
||||
DomainKind::Resonance => "resonance",
|
||||
DomainKind::Boundary => "boundary",
|
||||
DomainKind::Echo => "echo",
|
||||
}
|
||||
}
|
||||
|
||||
/// Distinct odd mixing constant per kind. These drive the nonlinear
|
||||
/// avalanche in the runtime and guarantee each domain transforms state
|
||||
/// differently from every other domain.
|
||||
pub fn mix_const(self) -> u64 {
|
||||
match self {
|
||||
DomainKind::Aether => 0x9e3779b97f4a7c15,
|
||||
DomainKind::Matter => 0xc2b2ae3d27d4eb4f,
|
||||
DomainKind::Flux => 0x165667b19e3779f9,
|
||||
DomainKind::Mind => 0x27d4eb2f165667c5,
|
||||
DomainKind::Entropy => 0x2545f4914f6cdd1d,
|
||||
DomainKind::Resonance => 0x85ebca77c2b2ae63,
|
||||
DomainKind::Boundary => 0xff51afd7ed558ccd,
|
||||
DomainKind::Echo => 0xc4ceb9fe1a85ec53,
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-kind rotation amount (kept in 1..63).
|
||||
pub fn rotate(self) -> u32 {
|
||||
7 + (self.index() as u32) * 7 % 53 + 1
|
||||
}
|
||||
|
||||
pub fn id(self) -> DomainId {
|
||||
DomainId(self.index() as u8)
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-domain state: observed and hidden lanes.
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct DomainState {
|
||||
pub kind: DomainKind,
|
||||
pub observed: [i64; LANES],
|
||||
pub hidden: [i64; HIDDEN_LANES],
|
||||
}
|
||||
|
||||
impl DomainState {
|
||||
pub fn new(kind: DomainKind) -> Self {
|
||||
DomainState {
|
||||
kind,
|
||||
observed: [0; LANES],
|
||||
hidden: [0; HIDDEN_LANES],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> DomainId {
|
||||
self.kind.id()
|
||||
}
|
||||
|
||||
/// Hash mixing kind + all state. Used inside replay/behavior hashes.
|
||||
pub fn hash_into(&self, h: &mut Hasher) {
|
||||
h.write_tag("domain");
|
||||
h.write_u8(self.kind.index() as u8);
|
||||
for &v in &self.observed {
|
||||
h.write_i64(v);
|
||||
}
|
||||
for &v in &self.hidden {
|
||||
h.write_i64(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What a domain currently exposes to be read.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ReadSurface {
|
||||
pub domain: DomainId,
|
||||
pub observed: Vec<i64>,
|
||||
pub hidden: Vec<i64>,
|
||||
}
|
||||
|
||||
/// What a domain currently allows to be written.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WriteSurface {
|
||||
pub domain: DomainId,
|
||||
pub writable_observed: Vec<usize>,
|
||||
pub writable_hidden: Vec<usize>,
|
||||
}
|
||||
|
||||
/// A structural+state fingerprint of a domain. Different kinds must produce
|
||||
/// different fingerprints (checked by the generators as "nonuniform domain
|
||||
/// fingerprints").
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub struct DomainFingerprint {
|
||||
pub domain: DomainId,
|
||||
pub hash: Hash,
|
||||
}
|
||||
|
||||
/// The trait every domain exposes (per spec).
|
||||
pub trait WorldDomain {
|
||||
fn domain_id(&self) -> DomainId;
|
||||
fn read_surface(&self) -> ReadSurface;
|
||||
fn write_surface(&self) -> WriteSurface;
|
||||
fn perturbation_axes(&self) -> Vec<Box<dyn PerturbationAxis>>;
|
||||
fn fingerprint(&self) -> DomainFingerprint;
|
||||
}
|
||||
|
||||
impl WorldDomain for DomainState {
|
||||
fn domain_id(&self) -> DomainId {
|
||||
self.kind.id()
|
||||
}
|
||||
|
||||
fn read_surface(&self) -> ReadSurface {
|
||||
ReadSurface {
|
||||
domain: self.kind.id(),
|
||||
observed: self.observed.to_vec(),
|
||||
hidden: self.hidden.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_surface(&self) -> WriteSurface {
|
||||
WriteSurface {
|
||||
domain: self.kind.id(),
|
||||
writable_observed: (0..LANES).collect(),
|
||||
writable_hidden: (0..HIDDEN_LANES).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn perturbation_axes(&self) -> Vec<Box<dyn PerturbationAxis>> {
|
||||
let d = self.kind.id();
|
||||
// Each domain exposes several distinct axes derived from its surface.
|
||||
// Across 8 domains this is well above the required 10 axes per case.
|
||||
let mut axes: Vec<Box<dyn PerturbationAxis>> = Vec::new();
|
||||
axes.push(Box::new(LaneBumpAxis {
|
||||
domain: d,
|
||||
lane: 0,
|
||||
delta: 1,
|
||||
}));
|
||||
axes.push(Box::new(LaneBumpAxis {
|
||||
domain: d,
|
||||
lane: (self.kind.index() % LANES),
|
||||
delta: -3,
|
||||
}));
|
||||
axes.push(Box::new(LaneScaleAxis {
|
||||
domain: d,
|
||||
lane: (self.kind.index() + 1) % LANES,
|
||||
factor: 3,
|
||||
}));
|
||||
axes.push(Box::new(HiddenFlipAxis {
|
||||
domain: d,
|
||||
lane: self.kind.index() % HIDDEN_LANES,
|
||||
}));
|
||||
axes.push(Box::new(LaneSwapAxis {
|
||||
domain: d,
|
||||
lane_a: 0,
|
||||
lane_b: (self.kind.index() % (LANES - 1)) + 1,
|
||||
}));
|
||||
axes
|
||||
}
|
||||
|
||||
fn fingerprint(&self) -> DomainFingerprint {
|
||||
let mut h = Hasher::new();
|
||||
h.write_tag("domain-fingerprint");
|
||||
h.write_u8(self.kind.index() as u8);
|
||||
h.write_u64(self.kind.mix_const());
|
||||
self.hash_into(&mut h);
|
||||
DomainFingerprint {
|
||||
domain: self.kind.id(),
|
||||
hash: h.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
//! `world_model` — the foundational crate. Defines world state, the eight
|
||||
//! independent domains, perturbation axes, execution contexts, world deltas,
|
||||
//! and the deterministic primitives (ids, stable hashing, RNG) used by every
|
||||
//! other crate.
|
||||
|
||||
pub mod context;
|
||||
pub mod domain;
|
||||
pub mod perturb;
|
||||
pub mod primitives;
|
||||
pub mod world;
|
||||
|
||||
pub use context::{
|
||||
standard_executors, ExecutionContext, ExecutorKind, ExecutorProfile, ALL_EXECUTOR_KINDS,
|
||||
};
|
||||
pub use domain::{
|
||||
DomainFingerprint, DomainKind, DomainState, ReadSurface, WorldDomain, WriteSurface,
|
||||
ALL_DOMAIN_KINDS,
|
||||
};
|
||||
pub use perturb::{
|
||||
HiddenFlipAxis, LaneBumpAxis, LaneScaleAxis, LaneSwapAxis, PerturbationAxis,
|
||||
TraceDifferenceExpectation,
|
||||
};
|
||||
pub use primitives::{
|
||||
combine_hashes, hash_i64_slice, ContractId, DomainId, Hash, Hasher, ProgramId, Rng, WorldId,
|
||||
HIDDEN_LANES, LANES, NUM_DOMAINS,
|
||||
};
|
||||
pub use world::{
|
||||
CausalState, DomainDelta, ExecutionState, ObservationState, ScheduledEffect, TimeState,
|
||||
WorldDelta, WorldSnapshot, REGS,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rng_is_deterministic() {
|
||||
let mut a = Rng::new(42);
|
||||
let mut b = Rng::new(42);
|
||||
for _ in 0..1000 {
|
||||
assert_eq!(a.next_u64(), b.next_u64());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hashing_is_stable_and_sensitive() {
|
||||
let h1 = hash_i64_slice("t", &[1, 2, 3]);
|
||||
let h2 = hash_i64_slice("t", &[1, 2, 3]);
|
||||
let h3 = hash_i64_slice("t", &[1, 2, 4]);
|
||||
assert_eq!(h1, h2);
|
||||
assert_ne!(h1, h3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn domain_fingerprints_are_nonuniform() {
|
||||
let w = WorldSnapshot::blank(WorldId(1), 7);
|
||||
let mut prints: Vec<_> = w.domains.iter().map(|d| d.fingerprint().hash).collect();
|
||||
prints.sort();
|
||||
prints.dedup();
|
||||
// even with identical (zero) state, distinct kinds give distinct prints
|
||||
assert_eq!(prints.len(), NUM_DOMAINS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perturbation_changes_world() {
|
||||
let mut w = WorldSnapshot::blank(WorldId(1), 7);
|
||||
w.domains[0].observed[0] = 100;
|
||||
let before = w.content_hash();
|
||||
let axis = LaneBumpAxis { domain: DomainId(0), lane: 0, delta: 5 };
|
||||
let w2 = axis.apply(&w);
|
||||
assert_ne!(before, w2.content_hash());
|
||||
assert_eq!(w2.domains[0].observed[0], 105);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
//! Perturbation axes. Perturbations are derived from domain surfaces (not a
|
||||
//! fixed global list) and each declares what trace/world difference it is
|
||||
//! expected to cause. The metamorphic gates check that these expectations
|
||||
//! actually hold across the corpus.
|
||||
|
||||
use crate::primitives::DomainId;
|
||||
use crate::world::WorldSnapshot;
|
||||
|
||||
/// What difference a perturbation is expected to produce.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct TraceDifferenceExpectation {
|
||||
pub expect_trace_change: bool,
|
||||
pub expect_delta_change: bool,
|
||||
pub expect_future_change: bool,
|
||||
/// If the perturbation is allowed to be observationally neutral, this
|
||||
/// explains why (spec allows <=5% neutral *with explanation*).
|
||||
pub neutral_explanation: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl TraceDifferenceExpectation {
|
||||
pub fn active() -> Self {
|
||||
TraceDifferenceExpectation {
|
||||
expect_trace_change: true,
|
||||
expect_delta_change: true,
|
||||
expect_future_change: true,
|
||||
neutral_explanation: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A perturbation axis derived from a domain surface.
|
||||
pub trait PerturbationAxis {
|
||||
fn name(&self) -> String;
|
||||
fn target(&self) -> DomainId;
|
||||
fn apply(&self, world: &WorldSnapshot) -> WorldSnapshot;
|
||||
fn expected_trace_difference(&self) -> TraceDifferenceExpectation;
|
||||
}
|
||||
|
||||
fn domain_mut<'a>(world: &'a mut WorldSnapshot, d: DomainId) -> &'a mut crate::domain::DomainState {
|
||||
&mut world.domains[d.0 as usize]
|
||||
}
|
||||
|
||||
/// Add a delta to an observed lane.
|
||||
pub struct LaneBumpAxis {
|
||||
pub domain: DomainId,
|
||||
pub lane: usize,
|
||||
pub delta: i64,
|
||||
}
|
||||
|
||||
impl PerturbationAxis for LaneBumpAxis {
|
||||
fn name(&self) -> String {
|
||||
format!("bump(d{},l{},{:+})", self.domain.0, self.lane, self.delta)
|
||||
}
|
||||
fn target(&self) -> DomainId {
|
||||
self.domain
|
||||
}
|
||||
fn apply(&self, world: &WorldSnapshot) -> WorldSnapshot {
|
||||
let mut w = world.clone();
|
||||
let ds = domain_mut(&mut w, self.domain);
|
||||
ds.observed[self.lane] = ds.observed[self.lane].wrapping_add(self.delta);
|
||||
w.mark_perturbed();
|
||||
w
|
||||
}
|
||||
fn expected_trace_difference(&self) -> TraceDifferenceExpectation {
|
||||
TraceDifferenceExpectation::active()
|
||||
}
|
||||
}
|
||||
|
||||
/// Multiply an observed lane by a factor.
|
||||
pub struct LaneScaleAxis {
|
||||
pub domain: DomainId,
|
||||
pub lane: usize,
|
||||
pub factor: i64,
|
||||
}
|
||||
|
||||
impl PerturbationAxis for LaneScaleAxis {
|
||||
fn name(&self) -> String {
|
||||
format!("scale(d{},l{},x{})", self.domain.0, self.lane, self.factor)
|
||||
}
|
||||
fn target(&self) -> DomainId {
|
||||
self.domain
|
||||
}
|
||||
fn apply(&self, world: &WorldSnapshot) -> WorldSnapshot {
|
||||
let mut w = world.clone();
|
||||
let ds = domain_mut(&mut w, self.domain);
|
||||
ds.observed[self.lane] = ds.observed[self.lane].wrapping_mul(self.factor).wrapping_add(1);
|
||||
w.mark_perturbed();
|
||||
w
|
||||
}
|
||||
fn expected_trace_difference(&self) -> TraceDifferenceExpectation {
|
||||
TraceDifferenceExpectation::active()
|
||||
}
|
||||
}
|
||||
|
||||
/// Flip the sign / toggle a hidden lane. Hidden changes may be observationally
|
||||
/// neutral on the immediate delta but should still influence future turns.
|
||||
pub struct HiddenFlipAxis {
|
||||
pub domain: DomainId,
|
||||
pub lane: usize,
|
||||
}
|
||||
|
||||
impl PerturbationAxis for HiddenFlipAxis {
|
||||
fn name(&self) -> String {
|
||||
format!("hidden(d{},l{})", self.domain.0, self.lane)
|
||||
}
|
||||
fn target(&self) -> DomainId {
|
||||
self.domain
|
||||
}
|
||||
fn apply(&self, world: &WorldSnapshot) -> WorldSnapshot {
|
||||
let mut w = world.clone();
|
||||
let ds = domain_mut(&mut w, self.domain);
|
||||
ds.hidden[self.lane] = !ds.hidden[self.lane].wrapping_add(0x5bd1e995);
|
||||
w.mark_perturbed();
|
||||
w
|
||||
}
|
||||
fn expected_trace_difference(&self) -> TraceDifferenceExpectation {
|
||||
// Hidden state feeds the runtime, so we still expect change; but it is
|
||||
// permitted to be observationally neutral on the immediate delta.
|
||||
TraceDifferenceExpectation {
|
||||
expect_trace_change: true,
|
||||
expect_delta_change: false,
|
||||
expect_future_change: true,
|
||||
neutral_explanation: Some("hidden lane influences future, not immediate observed delta"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Swap two observed lanes.
|
||||
pub struct LaneSwapAxis {
|
||||
pub domain: DomainId,
|
||||
pub lane_a: usize,
|
||||
pub lane_b: usize,
|
||||
}
|
||||
|
||||
impl PerturbationAxis for LaneSwapAxis {
|
||||
fn name(&self) -> String {
|
||||
format!("swap(d{},l{}<->l{})", self.domain.0, self.lane_a, self.lane_b)
|
||||
}
|
||||
fn target(&self) -> DomainId {
|
||||
self.domain
|
||||
}
|
||||
fn apply(&self, world: &WorldSnapshot) -> WorldSnapshot {
|
||||
let mut w = world.clone();
|
||||
let ds = domain_mut(&mut w, self.domain);
|
||||
ds.observed.swap(self.lane_a, self.lane_b);
|
||||
w.mark_perturbed();
|
||||
w
|
||||
}
|
||||
fn expected_trace_difference(&self) -> TraceDifferenceExpectation {
|
||||
TraceDifferenceExpectation::active()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
//! Deterministic primitives shared across the whole framework: stable ids,
|
||||
//! a stable content hash, and a deterministic RNG. Everything here is
|
||||
//! reproducible from a seed so that replay is bit-exact.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// Number of independent world domains. The spec mandates >= 8.
|
||||
pub const NUM_DOMAINS: usize = 8;
|
||||
/// Observed value lanes per domain.
|
||||
pub const LANES: usize = 4;
|
||||
/// Hidden (unobserved) value lanes per domain. These create the
|
||||
/// hidden/observed state divergence the spec requires.
|
||||
pub const HIDDEN_LANES: usize = 2;
|
||||
|
||||
macro_rules! id_type {
|
||||
($name:ident, $inner:ty) => {
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct $name(pub $inner);
|
||||
impl fmt::Debug for $name {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}({})", stringify!($name), self.0)
|
||||
}
|
||||
}
|
||||
impl fmt::Display for $name {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
id_type!(WorldId, u64);
|
||||
id_type!(ProgramId, u64);
|
||||
id_type!(ContractId, u64);
|
||||
|
||||
/// Identifies one of the [`NUM_DOMAINS`] domains.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct DomainId(pub u8);
|
||||
|
||||
impl fmt::Debug for DomainId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "DomainId({})", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A stable 64-bit content hash. Used for canonical comparison, replay
|
||||
/// hashes, and behavior fingerprints. Implemented with FNV-1a so the value
|
||||
/// is identical across machines and runs.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||
pub struct Hash(pub u64);
|
||||
|
||||
impl fmt::Debug for Hash {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Hash({:016x})", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Hash {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:016x}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
|
||||
const FNV_PRIME: u64 = 0x100000001b3;
|
||||
|
||||
/// Streaming stable hasher (FNV-1a, 64 bit).
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Hasher {
|
||||
state: u64,
|
||||
}
|
||||
|
||||
impl Default for Hasher {
|
||||
fn default() -> Self {
|
||||
Hasher { state: FNV_OFFSET }
|
||||
}
|
||||
}
|
||||
|
||||
impl Hasher {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn write_u8(&mut self, b: u8) {
|
||||
self.state ^= b as u64;
|
||||
self.state = self.state.wrapping_mul(FNV_PRIME);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn write_u64(&mut self, v: u64) {
|
||||
for i in 0..8 {
|
||||
self.write_u8(((v >> (i * 8)) & 0xff) as u8);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn write_i64(&mut self, v: i64) {
|
||||
self.write_u64(v as u64);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn write_usize(&mut self, v: usize) {
|
||||
self.write_u64(v as u64);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn write_bytes(&mut self, bytes: &[u8]) {
|
||||
for &b in bytes {
|
||||
self.write_u8(b);
|
||||
}
|
||||
}
|
||||
|
||||
/// Mix in a label so structurally different streams that happen to share
|
||||
/// numbers do not collide.
|
||||
#[inline]
|
||||
pub fn write_tag(&mut self, tag: &str) {
|
||||
self.write_bytes(tag.as_bytes());
|
||||
self.write_u8(0xff);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn finish(&self) -> Hash {
|
||||
Hash(self.state)
|
||||
}
|
||||
}
|
||||
|
||||
/// Hash a slice of i64 with a tag.
|
||||
pub fn hash_i64_slice(tag: &str, vals: &[i64]) -> Hash {
|
||||
let mut h = Hasher::new();
|
||||
h.write_tag(tag);
|
||||
h.write_usize(vals.len());
|
||||
for &v in vals {
|
||||
h.write_i64(v);
|
||||
}
|
||||
h.finish()
|
||||
}
|
||||
|
||||
/// Combine several hashes into one (order sensitive).
|
||||
pub fn combine_hashes(tag: &str, hashes: &[Hash]) -> Hash {
|
||||
let mut h = Hasher::new();
|
||||
h.write_tag(tag);
|
||||
for hh in hashes {
|
||||
h.write_u64(hh.0);
|
||||
}
|
||||
h.finish()
|
||||
}
|
||||
|
||||
/// Deterministic SplitMix64 RNG. Fully reproducible from a seed.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Rng {
|
||||
state: u64,
|
||||
}
|
||||
|
||||
impl Rng {
|
||||
pub fn new(seed: u64) -> Self {
|
||||
// Avoid the trivial all-zero state.
|
||||
Rng {
|
||||
state: seed ^ 0x9e3779b97f4a7c15,
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive a sub-stream from a seed and a label, so independent concerns
|
||||
/// never accidentally share a stream.
|
||||
pub fn derive(seed: u64, tag: &str) -> Self {
|
||||
let mut h = Hasher::new();
|
||||
h.write_tag(tag);
|
||||
h.write_u64(seed);
|
||||
Rng::new(h.finish().0)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn next_u64(&mut self) -> u64 {
|
||||
self.state = self.state.wrapping_add(0x9e3779b97f4a7c15);
|
||||
let mut z = self.state;
|
||||
z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
|
||||
z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb);
|
||||
z ^ (z >> 31)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn next_i64(&mut self) -> i64 {
|
||||
self.next_u64() as i64
|
||||
}
|
||||
|
||||
/// Uniform-ish integer in `[0, n)`.
|
||||
#[inline]
|
||||
pub fn below(&mut self, n: usize) -> usize {
|
||||
if n == 0 {
|
||||
return 0;
|
||||
}
|
||||
(self.next_u64() % (n as u64)) as usize
|
||||
}
|
||||
|
||||
/// Integer in `[lo, hi]` inclusive.
|
||||
#[inline]
|
||||
pub fn range_i64(&mut self, lo: i64, hi: i64) -> i64 {
|
||||
if hi <= lo {
|
||||
return lo;
|
||||
}
|
||||
let span = (hi - lo) as u64 + 1;
|
||||
lo + (self.next_u64() % span) as i64
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn chance(&mut self, p: f64) -> bool {
|
||||
(self.next_u64() as f64 / u64::MAX as f64) < p
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn next_bool(&mut self) -> bool {
|
||||
self.next_u64() & 1 == 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
//! The world snapshot and its constituent state machines.
|
||||
|
||||
use crate::domain::{DomainState, ALL_DOMAIN_KINDS};
|
||||
use crate::primitives::{
|
||||
DomainId, Hash, Hasher, Rng, HIDDEN_LANES, LANES, NUM_DOMAINS,
|
||||
};
|
||||
|
||||
/// Number of world-level execution accumulator registers.
|
||||
pub const REGS: usize = 4;
|
||||
|
||||
/// Cross-domain coupling. A dense `NUM_DOMAINS x NUM_DOMAINS` weight matrix
|
||||
/// that governs how a change in one domain propagates into others when turns
|
||||
/// advance. Generated per world; high rank by construction.
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct CausalState {
|
||||
pub coupling: [[i64; NUM_DOMAINS]; NUM_DOMAINS],
|
||||
}
|
||||
|
||||
impl CausalState {
|
||||
pub fn hash_into(&self, h: &mut Hasher) {
|
||||
h.write_tag("causal");
|
||||
for row in &self.coupling {
|
||||
for &v in row {
|
||||
h.write_i64(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Which lanes are observable, plus a deterministic observation-noise seed.
|
||||
/// Drives the divergence between hidden ground truth and observed projection.
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct ObservationState {
|
||||
pub visible: [[bool; LANES]; NUM_DOMAINS],
|
||||
pub noise_seed: u64,
|
||||
}
|
||||
|
||||
impl ObservationState {
|
||||
pub fn hash_into(&self, h: &mut Hasher) {
|
||||
h.write_tag("observation");
|
||||
for row in &self.visible {
|
||||
for &b in row {
|
||||
h.write_u8(b as u8);
|
||||
}
|
||||
}
|
||||
h.write_u64(self.noise_seed);
|
||||
}
|
||||
}
|
||||
|
||||
/// World-level execution accumulators carried across runes.
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct ExecutionState {
|
||||
pub accumulator: [i64; REGS],
|
||||
}
|
||||
|
||||
impl ExecutionState {
|
||||
pub fn hash_into(&self, h: &mut Hasher) {
|
||||
h.write_tag("execstate");
|
||||
for &v in &self.accumulator {
|
||||
h.write_i64(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A future effect scheduled by execution; resolved when turns advance.
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct ScheduledEffect {
|
||||
pub turn_offset: u8,
|
||||
pub domain: DomainId,
|
||||
pub lane: usize,
|
||||
pub hidden: bool,
|
||||
pub value: i64,
|
||||
}
|
||||
|
||||
/// Temporal state: pending scheduled effects create genuine future dependence.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Default)]
|
||||
pub struct TimeState {
|
||||
pub pending: Vec<ScheduledEffect>,
|
||||
}
|
||||
|
||||
impl TimeState {
|
||||
pub fn hash_into(&self, h: &mut Hasher) {
|
||||
h.write_tag("time");
|
||||
h.write_usize(self.pending.len());
|
||||
for e in &self.pending {
|
||||
h.write_u8(e.turn_offset);
|
||||
h.write_u8(e.domain.0);
|
||||
h.write_usize(e.lane);
|
||||
h.write_u8(e.hidden as u8);
|
||||
h.write_i64(e.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The full world snapshot (per spec).
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct WorldSnapshot {
|
||||
pub id: crate::primitives::WorldId,
|
||||
pub turn: u64,
|
||||
pub domains: Vec<DomainState>,
|
||||
pub causal_state: CausalState,
|
||||
pub observation_state: ObservationState,
|
||||
pub execution_state: ExecutionState,
|
||||
pub time_state: TimeState,
|
||||
pub seed: u64,
|
||||
/// Provenance counter for perturbations; never consumed by the runtime.
|
||||
pub perturb_nonce: u64,
|
||||
}
|
||||
|
||||
impl WorldSnapshot {
|
||||
/// An all-zero baseline world (the generators fill it with real state).
|
||||
pub fn blank(id: crate::primitives::WorldId, seed: u64) -> Self {
|
||||
let domains = ALL_DOMAIN_KINDS.iter().map(|&k| DomainState::new(k)).collect();
|
||||
WorldSnapshot {
|
||||
id,
|
||||
turn: 0,
|
||||
domains,
|
||||
causal_state: CausalState {
|
||||
coupling: [[0; NUM_DOMAINS]; NUM_DOMAINS],
|
||||
},
|
||||
observation_state: ObservationState {
|
||||
visible: [[true; LANES]; NUM_DOMAINS],
|
||||
noise_seed: seed ^ 0xa5a5a5a5,
|
||||
},
|
||||
execution_state: ExecutionState {
|
||||
accumulator: [0; REGS],
|
||||
},
|
||||
time_state: TimeState::default(),
|
||||
seed,
|
||||
perturb_nonce: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_perturbed(&mut self) {
|
||||
self.perturb_nonce = self.perturb_nonce.wrapping_add(1);
|
||||
}
|
||||
|
||||
pub fn domain(&self, d: DomainId) -> &DomainState {
|
||||
&self.domains[d.0 as usize]
|
||||
}
|
||||
|
||||
pub fn domain_mut(&mut self, d: DomainId) -> &mut DomainState {
|
||||
&mut self.domains[d.0 as usize]
|
||||
}
|
||||
|
||||
/// Observed projection: only visible observed lanes, with deterministic
|
||||
/// observation noise. Hidden lanes are excluded entirely. This is what an
|
||||
/// outside observer can measure, and differs from ground truth.
|
||||
pub fn observed_projection(&self) -> Vec<i64> {
|
||||
let mut rng = Rng::new(self.observation_state.noise_seed ^ self.turn);
|
||||
let mut out = Vec::with_capacity(NUM_DOMAINS * LANES);
|
||||
for (di, d) in self.domains.iter().enumerate() {
|
||||
for lane in 0..LANES {
|
||||
if self.observation_state.visible[di][lane] {
|
||||
let noise = rng.range_i64(-1, 1);
|
||||
out.push(d.observed[lane].wrapping_add(noise));
|
||||
} else {
|
||||
out.push(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Ground-truth state vector (observed + hidden), no noise. Used by the
|
||||
/// runtime and by canonical hashing.
|
||||
pub fn ground_truth(&self) -> Vec<i64> {
|
||||
let mut out = Vec::with_capacity(NUM_DOMAINS * (LANES + HIDDEN_LANES));
|
||||
for d in &self.domains {
|
||||
out.extend_from_slice(&d.observed);
|
||||
out.extend_from_slice(&d.hidden);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn content_hash(&self) -> Hash {
|
||||
let mut h = Hasher::new();
|
||||
h.write_tag("world");
|
||||
h.write_u64(self.id.0);
|
||||
h.write_u64(self.turn);
|
||||
h.write_u64(self.seed);
|
||||
for d in &self.domains {
|
||||
d.hash_into(&mut h);
|
||||
}
|
||||
self.causal_state.hash_into(&mut h);
|
||||
self.observation_state.hash_into(&mut h);
|
||||
self.execution_state.hash_into(&mut h);
|
||||
self.time_state.hash_into(&mut h);
|
||||
h.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Difference of a single domain (after - before, wrapping).
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct DomainDelta {
|
||||
pub domain: DomainId,
|
||||
pub observed: [i64; LANES],
|
||||
pub hidden: [i64; HIDDEN_LANES],
|
||||
}
|
||||
|
||||
impl DomainDelta {
|
||||
pub fn is_zero(&self) -> bool {
|
||||
self.observed.iter().all(|&v| v == 0) && self.hidden.iter().all(|&v| v == 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// The change produced by an execution.
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct WorldDelta {
|
||||
pub domain_deltas: Vec<DomainDelta>,
|
||||
pub turn_advance: u64,
|
||||
}
|
||||
|
||||
impl WorldDelta {
|
||||
/// Compute `after - before`.
|
||||
pub fn between(before: &WorldSnapshot, after: &WorldSnapshot) -> WorldDelta {
|
||||
let mut deltas = Vec::with_capacity(NUM_DOMAINS);
|
||||
for i in 0..NUM_DOMAINS {
|
||||
let b = &before.domains[i];
|
||||
let a = &after.domains[i];
|
||||
let mut observed = [0i64; LANES];
|
||||
let mut hidden = [0i64; HIDDEN_LANES];
|
||||
for l in 0..LANES {
|
||||
observed[l] = a.observed[l].wrapping_sub(b.observed[l]);
|
||||
}
|
||||
for l in 0..HIDDEN_LANES {
|
||||
hidden[l] = a.hidden[l].wrapping_sub(b.hidden[l]);
|
||||
}
|
||||
deltas.push(DomainDelta {
|
||||
domain: b.id(),
|
||||
observed,
|
||||
hidden,
|
||||
});
|
||||
}
|
||||
WorldDelta {
|
||||
domain_deltas: deltas,
|
||||
turn_advance: after.turn.wrapping_sub(before.turn),
|
||||
}
|
||||
}
|
||||
|
||||
/// Domains that actually changed.
|
||||
pub fn touched_domains(&self) -> Vec<DomainId> {
|
||||
self.domain_deltas
|
||||
.iter()
|
||||
.filter(|d| !d.is_zero())
|
||||
.map(|d| d.domain)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn hash(&self) -> Hash {
|
||||
let mut h = Hasher::new();
|
||||
h.write_tag("world-delta");
|
||||
h.write_u64(self.turn_advance);
|
||||
for d in &self.domain_deltas {
|
||||
h.write_u8(d.domain.0);
|
||||
for &v in &d.observed {
|
||||
h.write_i64(v);
|
||||
}
|
||||
for &v in &d.hidden {
|
||||
h.write_i64(v);
|
||||
}
|
||||
}
|
||||
h.finish()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user