// State-identity golden tests. // // These tests verify that the StateResourceRegistry and :id keyword system // preserves state across recompilation, forks state for different identities, // shares state across operator-compatible changes, and isolates state for // incompatible resource kinds. #define CATCH_CONFIG_MAIN #include "../catch.hpp" #include "src/signal_engine/signal_engine.h" #include #include #include #include #include using namespace sig; namespace { struct Sample { double t; double expected; double tolerance = 1e-9; }; struct GoldenHarness { SignalEngine engine; double cell_values[MAX_CELLS] = {}; double hw_inputs[32] = {}; double outputs[MAX_OUTPUTS] = {}; double workspace[MAX_TOTAL_NODES] = {}; double prev_t = 0.0; double last_dt = 0.0; bool has_ticked = false; bool state_committed_this_step = false; explicit GoldenHarness(double bpm = 120.0, int beats_per_bar = 4) { engine.init_defaults(bpm, beats_per_bar); } EvalResult eval_result(const std::string& code) { return eval_cold(code.c_str(), static_cast(code.size()), engine); } void eval_ok(const std::string& code) { EvalResult r = eval_result(code); INFO("code: " << code); if (r.kind == EvalResult::Error && r.diagnostic_count > 0) { INFO("diagnostic: " << (r.diagnostics[0].message ? r.diagnostics[0].message : "")); INFO("suggestion: " << (r.diagnostics[0].suggestion ? r.diagnostics[0].suggestion : "")); } REQUIRE(r.kind != EvalResult::Error); engine.pool.rebuild_execution_order(); } void assign_ok(const char* output, const char* expr) { eval_ok(std::string("(") + output + " " + expr + ")"); } uint16_t output_index(const char* output_name) { SymbolID sym = internSymbol(output_name); uint16_t idx = GraphBuilder::resolve_output_index(sym); REQUIRE(idx != NODE_NONE); return idx; } double sample(const char* output_name, double t) { std::memset(outputs, 0, sizeof(outputs)); std::memset(workspace, 0, sizeof(workspace)); engine.cells.snapshot_values(cell_values, MAX_CELLS); last_dt = t - prev_t; if (engine.pool.state_slot_count > 0 && has_ticked && t != prev_t && !state_committed_this_step) { ExecutionContext state_ctx; state_ctx.t = t; state_ctx.dt = last_dt; state_ctx.cell_values = cell_values; state_ctx.hw_inputs = hw_inputs; state_ctx.data_pool = engine.cells.data_pool; state_ctx.data_offsets = engine.cells.data_offsets; state_ctx.data_lengths = engine.cells.data_lengths; state_ctx.prev_outputs = engine.pool.prev_output_values; state_ctx.output_values = outputs; state_ctx.workspace = workspace; execute_all_outputs(engine.pool, state_ctx); commit_state(engine.pool, workspace); state_committed_this_step = true; std::memset(workspace, 0, sizeof(workspace)); std::memset(outputs, 0, sizeof(outputs)); } ExecutionContext ctx; ctx.t = t; ctx.dt = last_dt; ctx.cell_values = cell_values; ctx.hw_inputs = hw_inputs; ctx.data_pool = engine.cells.data_pool; ctx.data_offsets = engine.cells.data_offsets; ctx.data_lengths = engine.cells.data_lengths; ctx.prev_outputs = engine.pool.prev_output_values; ctx.output_values = outputs; ctx.workspace = workspace; execute_all_outputs(engine.pool, ctx); return outputs[output_index(output_name)]; } double tick(const char* output_name, double t) { if (t != prev_t) { state_committed_this_step = false; } double value = sample(output_name, t); commit_outputs(engine.pool, outputs); has_ticked = true; prev_t = t; return value; } std::vector tick_sequence(const char* output, std::initializer_list times) { std::vector result; result.reserve(times.size()); for (double t_val : times) { result.push_back(tick(output, t_val)); } return result; } }; } // namespace // ============================================================================ // Test 1: Reorder with same :id preserves state // ============================================================================ TEST_CASE("State identity: reorder with same :id preserves state", "[golden][state_identity]") { GoldenHarness h; h.eval_ok("(a1 (phasor 1 :id \"p\"))"); // Tick several times to accumulate phase. // phasor(1) at dt=0.01 increments phase by 0.01 each tick. h.tick("a1", 0.0); h.tick("a1", 0.01); h.tick("a1", 0.02); h.tick("a1", 0.03); h.tick("a1", 0.04); // Read the accumulated phase from the state slot directly. // After 4 increments of dt=0.01 at freq=1, phase should be ~0.04. double phase_before = h.engine.pool.state_values[0]; INFO("phase_before: " << phase_before); REQUIRE(phase_before > 0.01); // definitely accumulated // Recompile the exact same expression — state must survive. h.eval_ok("(a1 (phasor 1 :id \"p\"))"); double phase_after = h.engine.pool.state_values[0]; INFO("phase_after: " << phase_after); REQUIRE(phase_after == Approx(phase_before).margin(1e-12)); // Verify the phasor continues from where it was — the next tick should // produce a value close to phase_before (not reset to 0). double val = h.tick("a1", 0.05); REQUIRE(val > phase_before * 0.5); // not reset to zero REQUIRE(val == Approx(phase_before + 0.01).margin(1e-6)); // advanced by one dt } // ============================================================================ // Test 2: Different :id forks state // ============================================================================ TEST_CASE("State identity: different :id forks state", "[golden][state_identity]") { GoldenHarness h; // Two phasors with different :ids should get different registry entries. h.eval_ok("(a1 (phasor 1 :id \"alpha\"))"); h.eval_ok("(a2 (phasor 2 :id \"beta\"))"); // Registry should have at least 2 entries. REQUIRE(h.engine.registry.entry_count >= 2); // Tick to accumulate different phases. h.tick("a1", 0.0); h.tick("a1", 0.01); h.tick("a2", 0.0); h.tick("a2", 0.01); // The two phasors have different frequencies, so state values should differ. // Find the slots for each :id. uint16_t slot_alpha = NODE_NONE; uint16_t slot_beta = NODE_NONE; for (uint16_t i = 0; i < h.engine.registry.entry_count; i++) { auto& e = h.engine.registry.entries[i]; if (e.key.state_id == internSymbol("alpha") && e.key.kind == ResourceKind::OscillatorPhase) { slot_alpha = e.slot_index; } if (e.key.state_id == internSymbol("beta") && e.key.kind == ResourceKind::OscillatorPhase) { slot_beta = e.slot_index; } } REQUIRE(slot_alpha != NODE_NONE); REQUIRE(slot_beta != NODE_NONE); REQUIRE(slot_alpha != slot_beta); // The two slots hold independent values — verify they are distinct. // Both phasors were ticked with the same times but at different // frequencies, so their accumulated phases must differ. REQUIRE(h.engine.pool.state_values[slot_alpha] != h.engine.pool.state_values[slot_beta]); } // ============================================================================ // Test 3: Operator-compatible change preserves phase // ============================================================================ TEST_CASE("State identity: operator-compatible change preserves phase", "[golden][state_identity]") { GoldenHarness h; // saw and tri-osc both use ResourceKind::OscillatorPhase, so sharing // an :id between them should resolve to the same state slot. h.eval_ok("(a1 (lfo/saw 1 :id \"x\"))"); // Accumulate phase. h.tick("a1", 0.0); h.tick("a1", 0.01); h.tick("a1", 0.02); h.tick("a1", 0.03); // Record the accumulated phase. // Find the slot for :id "x" with OscillatorPhase kind. uint16_t slot = NODE_NONE; for (uint16_t i = 0; i < h.engine.registry.entry_count; i++) { auto& e = h.engine.registry.entries[i]; if (e.key.state_id == internSymbol("x") && e.key.kind == ResourceKind::OscillatorPhase) { slot = e.slot_index; break; } } REQUIRE(slot != NODE_NONE); double phase_before = h.engine.pool.state_values[slot]; INFO("phase_before: " << phase_before); REQUIRE(phase_before > 0.0); // Recompile as tri-osc with the same :id. // The OscillatorPhase slot should be reused (same key). h.eval_ok("(a1 (lfo/tri 1 :id \"x\"))"); double phase_after = h.engine.pool.state_values[slot]; INFO("phase_after: " << phase_after); REQUIRE(phase_after == Approx(phase_before).margin(1e-12)); } // ============================================================================ // Test 4: Incompatible change gets separate resources // ============================================================================ TEST_CASE("State identity: incompatible change gets separate resources", "[golden][state_identity]") { GoldenHarness h; // phasor uses ResourceKind::OscillatorPhase; count uses Counter + // TriggerMemory + ResetLatch. Even with the same :id "x", they // should resolve to different registry entries / slots. h.eval_ok("(a1 (phasor 1 :id \"x\"))"); uint16_t entries_after_phasor = h.engine.registry.entry_count; INFO("entries after phasor: " << entries_after_phasor); REQUIRE(entries_after_phasor >= 1); // count with same :id "x" — needs Counter, TriggerMemory, ResetLatch. // Use a1 on a different output to avoid overwriting. // We compile as a separate output so both co-exist. h.eval_ok("(a2 (count (sqr beat) :id \"x\"))"); uint16_t entries_after_count = h.engine.registry.entry_count; INFO("entries after count: " << entries_after_count); // count allocates 3 slots (Counter, TriggerMemory, ResetLatch). // phasor allocated 1 (OscillatorPhase). Total should be at least 4. REQUIRE(entries_after_count >= entries_after_phasor + 3); // Verify the phasor slot is distinct from every count slot. uint16_t phasor_slot = NODE_NONE; std::vector count_slots; for (uint16_t i = 0; i < h.engine.registry.entry_count; i++) { auto& e = h.engine.registry.entries[i]; if (e.key.state_id == internSymbol("x")) { if (e.key.kind == ResourceKind::OscillatorPhase) { phasor_slot = e.slot_index; } else { count_slots.push_back(e.slot_index); } } } REQUIRE(phasor_slot != NODE_NONE); REQUIRE(count_slots.size() == 3); for (auto s : count_slots) { REQUIRE(s != phasor_slot); } } // ============================================================================ // Test 5: Init not replayed on recompile // ============================================================================ TEST_CASE("State identity: init not replayed on recompile", "[golden][state_identity]") { GoldenHarness h; // Compile a phasor with :phase 0.0 and :id "p", then accumulate. h.eval_ok("(a1 (phasor 2 :id \"p\" :phase 0.0))"); h.tick("a1", 0.0); h.tick("a1", 0.01); h.tick("a1", 0.02); h.tick("a1", 0.03); h.tick("a1", 0.04); // Phase should have accumulated significantly (freq=2, 4 ticks of dt=0.01). double phase_before = h.engine.pool.state_values[0]; INFO("phase_before: " << phase_before); REQUIRE(phase_before > 0.01); // Recompile the same expression — :phase 0.0 is an init hint, NOT a // reset command. The registry should find the existing slot and skip // re-initialization. h.eval_ok("(a1 (phasor 2 :id \"p\" :phase 0.0))"); double phase_after = h.engine.pool.state_values[0]; INFO("phase_after: " << phase_after); REQUIRE(phase_after == Approx(phase_before).margin(1e-12)); } // ============================================================================ // Test 6: Anonymous slots still work // ============================================================================ TEST_CASE("State identity: anonymous slots still work", "[golden][state_identity]") { GoldenHarness h; // No :id — should allocate state slots via the old anonymous path. h.eval_ok("(a1 (phasor 1))"); REQUIRE(h.engine.pool.state_slot_count >= 1); // Tick several times and verify the phasor accumulates phase. h.tick("a1", 0.0); h.tick("a1", 0.01); h.tick("a1", 0.02); h.tick("a1", 0.03); // After multiple ticks at freq=1, state should have accumulated. double phase = h.engine.pool.state_values[0]; INFO("accumulated anonymous phase: " << phase); REQUIRE(phase > 0.01); // Verify tick produces monotonically increasing values. double v3 = h.tick("a1", 0.04); double v4 = h.tick("a1", 0.05); REQUIRE(v4 > v3); } // ============================================================================ // Test 7: useq-clear resets registry // ============================================================================ TEST_CASE("State identity: useq-clear resets registry", "[golden][state_identity]") { GoldenHarness h; // Build up some registry state. h.eval_ok("(a1 (phasor 1 :id \"p1\"))"); h.eval_ok("(a2 (phasor 2 :id \"p2\"))"); // Tick to accumulate. h.tick("a1", 0.0); h.tick("a1", 0.01); h.tick("a2", 0.0); h.tick("a2", 0.01); REQUIRE(h.engine.registry.entry_count >= 2); REQUIRE(h.engine.pool.state_slot_count >= 2); // useq-clear should reset the registry. h.eval_ok("(useq-clear)"); REQUIRE(h.engine.registry.entry_count == 0); } // ============================================================================ // Additional: Named :id across multiple recompiles preserves continuity // ============================================================================ TEST_CASE("State identity: multiple recompiles with same :id are stable", "[golden][state_identity]") { GoldenHarness h; h.eval_ok("(a1 (phasor 1 :id \"stable\"))"); // Tick 10 times. for (int i = 0; i < 10; i++) { h.tick("a1", i * 0.01); } double phase_a = h.engine.pool.state_values[0]; // Recompile 5 times. Phase must not reset. for (int i = 0; i < 5; i++) { h.eval_ok("(a1 (phasor 1 :id \"stable\"))"); double phase_now = h.engine.pool.state_values[0]; REQUIRE(phase_now == Approx(phase_a).margin(1e-12)); } } // ============================================================================ // Additional: Registry entry_count does not grow on re-resolve // ============================================================================ TEST_CASE("State identity: re-resolve does not grow entry count", "[golden][state_identity]") { GoldenHarness h; h.eval_ok("(a1 (phasor 1 :id \"re\"))"); uint16_t count_after_first = h.engine.registry.entry_count; // Recompile same expression multiple times. h.eval_ok("(a1 (phasor 1 :id \"re\"))"); h.eval_ok("(a1 (phasor 1 :id \"re\"))"); h.eval_ok("(a1 (phasor 1 :id \"re\"))"); REQUIRE(h.engine.registry.entry_count == count_after_first); } TEST_CASE("State identity: one update writer owns an explicit id", "[golden][state_identity][ownership]") { GoldenHarness h; h.eval_ok("(a1 (phasor 1 :id \"owned-phase\"))"); uint16_t entries_before = h.engine.registry.entry_count; uint16_t slots_before = h.engine.pool.state_slot_count; EvalResult conflict = h.eval_result("(a2 (phasor 2 :id \"owned-phase\"))"); REQUIRE(conflict.kind == EvalResult::Error); REQUIRE(conflict.diagnostic_count >= 1); REQUIRE(conflict.diagnostics[0].category == DiagnosticCategory::Boundary); REQUIRE(h.engine.pool.outputs[h.output_index("a2")].root_node == NODE_NONE); REQUIRE(h.engine.registry.entry_count == entries_before); REQUIRE(h.engine.pool.state_slot_count == slots_before); // Recompiling the owning program is still legal and preserves state. h.eval_ok("(a1 (phasor 3 :id \"owned-phase\"))"); REQUIRE(h.engine.registry.entry_count == entries_before); REQUIRE(h.engine.pool.state_slot_count == slots_before); } TEST_CASE("State identity: retired program state slots are reused", "[golden][state_identity][reclaim]") { GoldenHarness h; h.eval_ok("(a1 (phasor 1 :id \"retired-phase\"))"); REQUIRE(h.engine.registry.entry_count == 1); REQUIRE(h.engine.pool.state_slot_count == 1); uint16_t retired_slot = h.engine.registry.entries[0].slot_index; // Publishing a pure replacement retires the old program's only writer. h.eval_ok("(a1 0.5)"); REQUIRE(h.engine.registry.entry_count == 0); REQUIRE(h.engine.registry.free_slot_count == 0); REQUIRE(h.engine.pool.state_slot_count == 0); // A different program starts again at the compacted dense slot zero, so // repeated edit/replacement cycles do not exhaust fixed firmware storage. h.eval_ok("(a2 (integrate 1 :id \"new-integrator\"))"); REQUIRE(h.engine.registry.entry_count == 1); REQUIRE(h.engine.registry.entries[0].slot_index == retired_slot); REQUIRE(h.engine.registry.free_slot_count == 0); REQUIRE(h.engine.pool.state_slot_count == 1); } // ============================================================================ // Projection fork: simulate save/restore cycle and verify invariants // ============================================================================ TEST_CASE("State identity: projection fork preserves live state", "[golden][state_identity][projection]") { GoldenHarness h; h.eval_ok("(a1 (phasor 1 :id \"proj-p\"))"); h.tick("a1", 0.0); h.tick("a1", 0.01); h.tick("a1", 0.02); // Snapshot live state before projection double live_state[MAX_STATE_SLOTS]; memcpy(live_state, h.engine.pool.state_values, sizeof(live_state)); uint16_t live_slot_count = h.engine.pool.state_slot_count; uint16_t live_registry_count = h.engine.registry.entry_count; double live_prev_outputs[MAX_OUTPUTS]; memcpy(live_prev_outputs, h.engine.pool.prev_output_values, sizeof(live_prev_outputs)); // Simulate projection fork: save → install fork → advance → restore // Save double saved_state[MAX_STATE_SLOTS]; uint16_t saved_slot_count = h.engine.pool.state_slot_count; StateResourceRegistry saved_registry = h.engine.registry; double saved_prev_outputs[MAX_OUTPUTS]; memcpy(saved_state, h.engine.pool.state_values, sizeof(saved_state)); memcpy(saved_prev_outputs, h.engine.pool.prev_output_values, sizeof(saved_prev_outputs)); // Execute several projection samples (advances state in the engine) for (int s = 0; s < 10; s++) { double t = 0.03 + s * 0.01; std::memset(h.outputs, 0, sizeof(h.outputs)); std::memset(h.workspace, 0, sizeof(h.workspace)); h.engine.cells.snapshot_values(h.cell_values, MAX_CELLS); ExecutionContext ctx; ctx.t = t; ctx.dt = 0.01; ctx.cell_values = h.cell_values; ctx.hw_inputs = h.hw_inputs; ctx.data_pool = h.engine.cells.data_pool; ctx.data_offsets = h.engine.cells.data_offsets; ctx.data_lengths = h.engine.cells.data_lengths; ctx.prev_outputs = h.engine.pool.prev_output_values; ctx.output_values = h.outputs; ctx.workspace = h.workspace; execute_all_outputs(h.engine.pool, ctx); commit_state(h.engine.pool, h.workspace); commit_outputs(h.engine.pool, h.outputs); } // State has been mutated by projection execution REQUIRE(h.engine.pool.state_values[0] != live_state[0]); // Restore live state memcpy(h.engine.pool.state_values, saved_state, sizeof(saved_state)); h.engine.pool.state_slot_count = saved_slot_count; h.engine.registry = saved_registry; memcpy(h.engine.pool.prev_output_values, saved_prev_outputs, sizeof(saved_prev_outputs)); // Verify live state is unchanged REQUIRE(h.engine.pool.state_slot_count == live_slot_count); REQUIRE(h.engine.registry.entry_count == live_registry_count); for (uint16_t i = 0; i < live_slot_count; i++) { REQUIRE(h.engine.pool.state_values[i] == Approx(live_state[i])); } for (uint16_t i = 0; i < MAX_OUTPUTS; i++) { REQUIRE(h.engine.pool.prev_output_values[i] == Approx(live_prev_outputs[i])); } } TEST_CASE("State identity: repeated projection doesn't grow registry", "[golden][state_identity][projection]") { GoldenHarness h; h.eval_ok("(a1 (phasor 1 :id \"rp\"))"); h.tick("a1", 0.0); uint16_t initial_entries = h.engine.registry.entry_count; uint16_t initial_slots = h.engine.pool.state_slot_count; // Simulate 5 projection fork cycles for (int cycle = 0; cycle < 5; cycle++) { double saved_state[MAX_STATE_SLOTS]; uint16_t saved_slot_count = h.engine.pool.state_slot_count; memcpy(saved_state, h.engine.pool.state_values, sizeof(saved_state)); // Advance 3 samples in "fork" for (int s = 0; s < 3; s++) { double t = 0.01 * (cycle * 3 + s + 1); std::memset(h.outputs, 0, sizeof(h.outputs)); std::memset(h.workspace, 0, sizeof(h.workspace)); h.engine.cells.snapshot_values(h.cell_values, MAX_CELLS); ExecutionContext ctx; ctx.t = t; ctx.dt = 0.01; ctx.cell_values = h.cell_values; ctx.hw_inputs = h.hw_inputs; ctx.data_pool = h.engine.cells.data_pool; ctx.data_offsets = h.engine.cells.data_offsets; ctx.data_lengths = h.engine.cells.data_lengths; ctx.prev_outputs = h.engine.pool.prev_output_values; ctx.output_values = h.outputs; ctx.workspace = h.workspace; execute_all_outputs(h.engine.pool, ctx); commit_state(h.engine.pool, h.workspace); } // Restore memcpy(h.engine.pool.state_values, saved_state, sizeof(saved_state)); h.engine.pool.state_slot_count = saved_slot_count; } REQUIRE(h.engine.registry.entry_count == initial_entries); REQUIRE(h.engine.pool.state_slot_count == initial_slots); }