// Signal engine golden semantics. // // These tests are intentionally written at the language boundary: eval source // text, sample named outputs, and assert user-visible values. Keep low-level // node-shape tests in test_signal_engine.cpp. #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; // track previous tick time for dt double last_dt = 0.0; // dt used for current tick bool has_ticked = false; // false until first tick completes bool state_committed_this_step = false; // prevents double-commit at same t 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; // State update: at the start of each NEW time-step (after the very // first tick), execute update graphs and commit state before outputs. if (engine.pool.state_slot_count > 0 && has_ticked && t != prev_t && !state_committed_this_step) { // Execute all nodes to evaluate state update subgraphs 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; // Clear workspace and outputs for the real output evaluation 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; } // ── Diagnostic assertion helpers ─────────────────────────────────────── // Assert that eval produces a diagnostic with the expected category. // Optionally check that message and suggestion contain given substrings. void expect_error(const std::string& code, DiagnosticCategory expected_cat, const char* message_contains = nullptr, const char* suggestion_contains = nullptr) { EvalResult r = eval_result(code); INFO("code: " << code); REQUIRE(r.kind == EvalResult::Error); REQUIRE(r.diagnostic_count > 0); bool found = false; for (uint8_t i = 0; i < r.diagnostic_count; ++i) { if (r.diagnostics[i].category == expected_cat) { found = true; if (message_contains) { REQUIRE(r.diagnostics[i].message != nullptr); INFO("expected message to contain: " << message_contains); INFO("actual message: " << r.diagnostics[i].message); REQUIRE(std::string(r.diagnostics[i].message) .find(message_contains) != std::string::npos); } if (suggestion_contains) { REQUIRE(r.diagnostics[i].suggestion != nullptr); INFO("expected suggestion to contain: " << suggestion_contains); INFO("actual suggestion: " << r.diagnostics[i].suggestion); REQUIRE(std::string(r.diagnostics[i].suggestion) .find(suggestion_contains) != std::string::npos); } break; } } INFO("expected category: " << category_to_cstr(expected_cat)); REQUIRE(found); } // Assert that eval produces a diagnostic at an exact span location. void expect_error_at(const std::string& code, DiagnosticCategory expected_cat, uint16_t span_start, uint16_t span_len, const char* message_contains = nullptr) { EvalResult r = eval_result(code); INFO("code: " << code); REQUIRE(r.kind == EvalResult::Error); REQUIRE(r.diagnostic_count > 0); bool found = false; for (uint8_t i = 0; i < r.diagnostic_count; ++i) { if (r.diagnostics[i].category == expected_cat) { found = true; INFO("expected span_start: " << span_start << " actual: " << r.diagnostics[i].span_start); REQUIRE(r.diagnostics[i].span_start == span_start); INFO("expected span_len: " << span_len << " actual: " << r.diagnostics[i].span_len); REQUIRE(r.diagnostics[i].span_len == span_len); if (message_contains) { REQUIRE(r.diagnostics[i].message != nullptr); REQUIRE(std::string(r.diagnostics[i].message) .find(message_contains) != std::string::npos); } break; } } REQUIRE(found); } // Assert that a suggestion from a diagnostic compiles successfully. // This validates that error suggestions are valid ModuLisp code. void expect_suggestion_compiles(const std::string& code, DiagnosticCategory expected_cat) { EvalResult r = eval_result(code); REQUIRE(r.kind == EvalResult::Error); REQUIRE(r.diagnostic_count > 0); for (uint8_t i = 0; i < r.diagnostic_count; ++i) { if (r.diagnostics[i].category == expected_cat && r.diagnostics[i].suggestion != nullptr) { std::string suggestion = r.diagnostics[i].suggestion; // Strip "Try: " prefix if present if (suggestion.substr(0, 5) == "Try: ") { suggestion = suggestion.substr(5); } // Only test suggestions that look like expressions // (start with '(' or are a bare symbol) if (!suggestion.empty() && suggestion[0] != 'C' && suggestion[0] != 'U' && suggestion[0] != 'E' && suggestion[0] != 'D') { // Wrap in output assignment to test compilability std::string test_code = "(a1 " + suggestion + ")"; EvalResult sr = eval_result(test_code); INFO("suggestion code: " << test_code); INFO("original error code: " << code); // NOTE: Some suggestions are just symbol names, which might // not be defined. We only check that parsing/compilation // doesn't produce a Syntax error. if (sr.kind == EvalResult::Error) { for (uint8_t j = 0; j < sr.diagnostic_count; ++j) { INFO("suggestion diagnostic: " << (sr.diagnostics[j].message ? sr.diagnostics[j].message : "")); REQUIRE(sr.diagnostics[j].category != DiagnosticCategory::Syntax); } } } break; } } } // ── Sampling helpers ─────────────────────────────────────────────────── // Sample across a time window and return values. std::vector sample_window(const char* output, double t_start, double t_end, size_t count) { std::vector result; result.reserve(count); for (size_t i = 0; i < count; ++i) { double t_val = t_start + (t_end - t_start) * static_cast(i) / static_cast(count - 1); result.push_back(sample(output, t_val)); } return result; } // ── Batch comparison helper ──────────────────────────────────────────── // Find the active-output index for batch buffer layout. // execute_batch packs only outputs with root_node != NODE_NONE, in // slot order. Returns the 0-based position of the requested output // within the active set. uint16_t batch_output_index(const char* output_name) { uint16_t slot = output_index(output_name); uint16_t active = 0; for (uint16_t o = 0; o < slot; o++) { if (engine.pool.outputs[o].root_node != NODE_NONE) active++; } return active; } // Verify batch execution matches single-sample across a time window. void verify_batch_matches_single(const char* output, double t_start, double t_end, size_t count, double tolerance = 1e-9) { engine.pool.allocate_batch_workspace(); std::vector times(count); for (size_t i = 0; i < count; ++i) { times[i] = t_start + (t_end - t_start) * static_cast(i) / static_cast(count - 1); } // Count active outputs for buffer sizing uint16_t active_count = 0; for (uint16_t o = 0; o < MAX_OUTPUTS; o++) { if (engine.pool.outputs[o].root_node != NODE_NONE) active_count++; } std::vector batch_buf(active_count * count, 0.0); engine.cells.snapshot_values(cell_values, MAX_CELLS); execute_batch(engine.pool, times.data(), count, cell_values, hw_inputs, engine.cells.data_pool, engine.cells.data_offsets, engine.cells.data_lengths, batch_buf.data(), active_count); uint16_t idx = batch_output_index(output); for (size_t i = 0; i < count; ++i) { INFO("sample index: " << i << " t: " << times[i]); double single_val = sample(output, times[i]); REQUIRE(batch_buf[idx * count + i] == Approx(single_val).margin(tolerance)); } } // ── Multi-tick helper ────────────────────────────────────────────────── // Execute a sequence of ticks and return committed values at each. 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; } }; void check_expr(const char* name, const char* expr, std::initializer_list samples, double bpm = 120.0, int beats_per_bar = 4, const char* setup = nullptr, const char* output = "a1") { DYNAMIC_SECTION(name) { GoldenHarness h(bpm, beats_per_bar); if (setup) h.eval_ok(setup); h.assign_ok(output, expr); for (const Sample& sample : samples) { INFO("expr: " << expr); INFO("t: " << sample.t); double actual = h.sample(output, sample.t); REQUIRE(actual == Approx(sample.expected).margin(sample.tolerance)); } } } EvalResult eval_output_error(const char* expr) { GoldenHarness h; std::string code = std::string("(a1 ") + expr + ")"; return h.eval_result(code); } } // namespace TEST_CASE("Golden semantics: numeric expressions are lifted into signals", "[golden][signal_engine][semantics]") { check_expr("constant arithmetic", "(+ 1 2)", {{0.0, 3.0}}); check_expr("variadic addition", "(+ 1 2 3 4)", {{0.0, 10.0}}); check_expr("variadic subtraction left folds", "(- 100 30 20 10)", {{0.0, 40.0}}); check_expr("unary negation form", "(- 5)", {{0.0, -5.0}}); check_expr("legacy pow order is exponent then base", "(pow 2 10)", {{0.0, 100.0}}); check_expr("standard expt order is base then exponent", "(expt 2 10)", {{0.0, 1024.0}}); check_expr("fractional arithmetic", "(+ 0.1 0.2)", {{0.0, 0.3, 1e-6}}); check_expr("division fraction", "(/ 1 3)", {{0.0, 1.0 / 3.0, 1e-9}}); check_expr("constant folding is time-invariant", "(+ (* 2 3) 4)", {{0.0, 10.0}, {0.5, 10.0}, {1.0, 10.0}}); check_expr("numeric truthiness treats any non-zero as true", "(if -0.1 7 9)", {{0.0, 7.0}}); check_expr("and returns numeric truth", "(and 2 -3)", {{0.0, 1.0}}); check_expr("or returns numeric truth", "(or 0 -2)", {{0.0, 1.0}}); check_expr("if without else defaults to zero", "(if 0 42)", {{0.0, 0.0}}); check_expr("clamp below range", "(clamp 0 3 -1)", {{0.0, 0.0}}); check_expr("clamp above range", "(clamp 0 3 5)", {{0.0, 3.0}}); check_expr("lerp midpoint", "(lerp 0 10 0.5)", {{0.0, 5.0}}); check_expr("scale unipolar value", "(scale 100 200 0.5)", {{0.0, 150.0}}); } TEST_CASE("Golden semantics: time, phasors, and time substitutions", "[golden][signal_engine][time]") { check_expr("raw t is seconds", "t", {{0.0, 0.0}, {1.25, 1.25}}); check_expr("beat phasor at 120 bpm", "beat", {{0.0, 0.0}, {0.125, 0.25}, {0.25, 0.5}, {0.5, 0.0}}); check_expr("beat phasor at 60 bpm", "beat", {{0.0, 0.0}, {0.5, 0.5}, {1.0, 0.0}}, 60.0); check_expr("bar phasor at 120 bpm 4/4", "bar", {{0.0, 0.0}, {0.5, 0.25}, {1.0, 0.5}, {2.0, 0.0}}); check_expr("phrase phasor uses bars-per-phrase default", "phrase", {{0.0, 0.0}, {4.0, 0.5}, {8.0, 0.0}}); check_expr("section phasor uses phrase defaults", "section", {{0.0, 0.0}, {16.0, 0.5}, {32.0, 0.0}}); check_expr("beat-num is an unwrapped counter", "beat-num", {{0.0, 0.0}, {0.49, 0.0}, {0.5, 1.0}, {1.25, 2.0}}); check_expr("bar-num is an unwrapped counter", "bar-num", {{0.0, 0.0}, {1.99, 0.0}, {2.0, 1.0}, {4.1, 2.0}}); check_expr("beat-dur follows bpm", "beat-dur", {{0.0, 0.5}}, 120.0); check_expr("bar-dur follows metre", "bar-dur", {{0.0, 1.5}}, 120.0, 3); check_expr("fast doubles local time", "(fast 2 beat)", {{0.125, 0.5}, {0.25, 0.0}}); check_expr("slow halves local time", "(slow 2 beat)", {{0.25, 0.25}, {0.5, 0.5}}); check_expr("nested constant warps compose", "(fast 2 (slow 4 beat))", {{0.5, 0.5}, {1.0, 0.0}}); check_expr("dynamic fast is pointwise substitution", "(fast (+ 1 beat) beat)", {{0.125, 0.3125}}); check_expr("offset uses seconds", "(offset 0.25 beat)", {{0.0, 0.5}, {0.125, 0.75}}); check_expr("shift aliases offset", "(shift 0.25 beat)", {{0.0, 0.5}, {0.125, 0.75}}); check_expr("musical offset can use beat-dur", "(offset (* 0.5 beat-dur) beat)", {{0.0, 0.5}}); } TEST_CASE("Golden semantics: waveform and sequence helpers", "[golden][signal_engine][sequences]") { check_expr("sin is unipolar sine over normalized phase", "(sin beat)", {{0.0, 0.5, 1e-9}, {0.125, 1.0, 1e-9}, {0.375, 0.0, 1e-9}}); check_expr("cos is unipolar cosine over normalized phase", "(cos beat)", {{0.0, 1.0, 1e-9}, {0.25, 0.0, 1e-9}}); check_expr("tri maps one phase value", "(tri 0.25)", {{0.0, 0.5, 1e-9}}); check_expr("sqr thresholds phase at half", "(sqr beat)", {{0.125, 1.0}, {0.375, 0.0}}); check_expr("pulse reads width before its final phasor", "(pulse 0.25 beat)", {{0.1, 1.0}, {0.2, 0.0}}); check_expr("bipolar to unipolar", "(b>u -1)", {{0.0, 0.0}}); check_expr("unipolar to bipolar", "(u>b 0.25)", {{0.0, -0.5}}); check_expr("step over literal vector", "(step [10 20 30 40] beat)", {{0.0, 10.0}, {0.125, 20.0}, {0.25, 30.0}, {0.375, 40.0}}); check_expr("from-list is stepped lookup", "(from-list [10 20 30] beat)", {{0.0, 10.0}, {0.25, 20.0}, {0.49, 30.0}}); check_expr("seq aliases from-list", "(seq [100 200] beat)", {{0.0, 100.0}, {0.25, 200.0}}); check_expr("interp linearly interpolates", "(interp [0 1 0] beat)", {{0.0, 0.0}, {0.125, 0.5}, {0.25, 1.0}, {0.375, 0.5}}); check_expr("gates follows pattern", "(gates [1 0 1 0] beat)", {{0.0, 1.0}, {0.125, 0.0}}); check_expr("trigs aliases gates", "(trigs [1 0 1 0] beat)", {{0.0, 1.0}, {0.125, 0.0}}); check_expr("euclid distributes hits", "(euclid 3 8 beat)", {{0.0, 1.0}, {0.125, 0.0}}); } TEST_CASE("Golden semantics: top-level cells, functions, and reactivity", "[golden][signal_engine][reactivity]") { check_expr("number cell reference", "freq", {{0.0, 440.0}}, 120.0, 4, "(define freq 440)"); check_expr("expression cell is time-varying", "sweep", {{0.0, 200.0}, {0.25, 250.0}}, 120.0, 4, "(define sweep (+ 200 (* 200 t)))"); check_expr("data cell feeds step", "(step pattern beat)", {{0.0, 10.0}, {0.25, 30.0}}, 120.0, 4, "(define pattern [10 20 30 40])"); check_expr("defn with one argument inlines at call site", "(double 5)", {{0.0, 10.0}}, 120.0, 4, "(defn double [x] (* x 2))"); check_expr("defn inherits caller time context", "(fast 2 (scaled-beat 3))", {{0.125, 1.5}}, 120.0, 4, "(defn scaled-beat [s] (* s beat))"); SECTION("redefining a depended-on cell recompiles output") { GoldenHarness h; h.eval_ok("(define freq 440)"); h.assign_ok("a1", "freq"); REQUIRE(h.sample("a1", 0.0) == Approx(440.0)); h.eval_ok("(define freq 880)"); REQUIRE(h.sample("a1", 0.0) == Approx(880.0)); } SECTION("function redefinition recompiles callers") { GoldenHarness h; h.eval_ok("(defn f [x] (* x 2))"); h.assign_ok("a1", "(f 5)"); REQUIRE(h.sample("a1", 0.0) == Approx(10.0)); h.eval_ok("(defn f [x] (* x 3))"); REQUIRE(h.sample("a1", 0.0) == Approx(15.0)); } } TEST_CASE("Golden semantics: outputs, prev, inputs, and batch execution", "[golden][signal_engine][execution]") { SECTION("unassigned outputs use neutral zero") { GoldenHarness h; REQUIRE(h.sample("a1", 0.0) == Approx(0.0)); REQUIRE(h.sample("d1", 0.0) == Approx(0.0)); } SECTION("bare output reference reads previous committed sample") { GoldenHarness h; h.eval_ok("(a1 0.75) (a2 a1)"); REQUIRE(h.tick("a2", 0.0) == Approx(0.0)); REQUIRE(h.tick("a2", 0.001) == Approx(0.75)); } SECTION("explicit prev reads previous committed sample") { GoldenHarness h; h.eval_ok("(d1 1.0) (a1 (prev d1))"); REQUIRE(h.tick("a1", 0.0) == Approx(0.0)); REQUIRE(h.tick("a1", 0.001) == Approx(1.0)); } SECTION("hardware input leaves read injected input values") { GoldenHarness h; h.hw_inputs[0] = 1.0; // in1 (INP_I1) h.hw_inputs[8] = 0.25; // ain1 (INP_AI1) h.assign_ok("a1", "(+ in1 ain1)"); REQUIRE(h.sample("a1", 0.0) == Approx(1.25)); } SECTION("batch execution matches single-sample execution") { GoldenHarness h; h.assign_ok("a1", "(+ (sin beat) (* 0.25 bar))"); h.engine.pool.allocate_batch_workspace(); constexpr size_t sample_count = 5; double times[sample_count] = {0.0, 0.125, 0.25, 0.375, 0.5}; double batch[MAX_OUTPUTS * sample_count] = {}; h.engine.cells.snapshot_values(h.cell_values, MAX_CELLS); execute_batch(h.engine.pool, times, sample_count, h.cell_values, h.hw_inputs, h.engine.cells.data_pool, h.engine.cells.data_offsets, h.engine.cells.data_lengths, batch, MAX_OUTPUTS); for (size_t i = 0; i < sample_count; ++i) { INFO("sample index: " << i); double single = h.sample("a1", times[i]); REQUIRE(batch[i] == Approx(single).margin(1e-9)); } } } TEST_CASE("Golden semantics: diagnostics and failure isolation", "[golden][signal_engine][diagnostics]") { SECTION("unknown names produce a diagnostic with fuzzy suggestions") { GoldenHarness h; h.eval_ok("(define freq 440)"); EvalResult r = h.eval_result("(a1 frq)"); REQUIRE(r.kind == EvalResult::Error); REQUIRE(r.diagnostic_count > 0); REQUIRE(r.diagnostics[0].severity == DiagnosticSeverity::Error); REQUIRE(r.diagnostics[0].span_start == 4); REQUIRE(r.diagnostics[0].span_len == 3); REQUIRE(r.diagnostics[0].message != nullptr); REQUIRE(std::string(r.diagnostics[0].message).find("freq") != std::string::npos); REQUIRE(r.diagnostics[0].suggestion != nullptr); } SECTION("side effects are rejected inside output expressions") { EvalResult r = eval_output_error("(define x 1)"); REQUIRE(r.kind == EvalResult::Error); REQUIRE(r.diagnostic_count > 0); REQUIRE(r.diagnostics[0].severity == DiagnosticSeverity::Error); REQUIRE(r.diagnostics[0].message != nullptr); REQUIRE(std::string(r.diagnostics[0].message).find("inside an output") != std::string::npos); } SECTION("recursion is rejected instead of hanging") { GoldenHarness h; h.eval_ok("(defn f [x] (f x))"); EvalResult r = h.eval_result("(a1 (f 1))"); REQUIRE(r.kind == EvalResult::Error); REQUIRE(r.diagnostic_count > 0); REQUIRE(r.diagnostics[0].message != nullptr); REQUIRE(std::string(r.diagnostics[0].message).find("recursive") != std::string::npos); } SECTION("compile error leaves previous output running") { GoldenHarness h; h.assign_ok("a1", "(+ beat 1)"); REQUIRE(h.sample("a1", 0.25) == Approx(1.5)); EvalResult bad = h.eval_result("(a1 (unknown-name beat))"); REQUIRE(bad.kind == EvalResult::Error); REQUIRE(h.sample("a1", 0.25) == Approx(1.5)); } SECTION("one output compile error does not affect another output") { GoldenHarness h; h.assign_ok("a1", "0.2"); h.assign_ok("a2", "0.8"); EvalResult bad = h.eval_result("(a1 (not-a-function 1))"); REQUIRE(bad.kind == EvalResult::Error); REQUIRE(h.sample("a1", 0.0) == Approx(0.2)); REQUIRE(h.sample("a2", 0.0) == Approx(0.8)); } SECTION("expect_error helper: unknown name produces error with message") { GoldenHarness h; h.expect_error("(a1 nonexistent)", DiagnosticCategory::UndefinedName, "Unknown"); } SECTION("expect_error_at helper: span points at the bad symbol") { GoldenHarness h; h.eval_ok("(define freq 440)"); // "(a1 frq)" — 'frq' starts at offset 4, length 3 h.expect_error_at("(a1 frq)", DiagnosticCategory::UndefinedName, 4, 3, "freq"); } SECTION("expect_error helper: side effects rejected") { GoldenHarness h; h.expect_error("(a1 (define x 1))", DiagnosticCategory::Boundary, "inside an output"); } } TEST_CASE("Golden semantics: sample_window and tick_sequence helpers", "[golden][signal_engine][helpers]") { SECTION("sample_window returns correct count and values") { GoldenHarness h; h.assign_ok("a1", "t"); auto window = h.sample_window("a1", 0.0, 1.0, 5); REQUIRE(window.size() == 5); REQUIRE(window[0] == Approx(0.0)); REQUIRE(window[1] == Approx(0.25)); REQUIRE(window[2] == Approx(0.5)); REQUIRE(window[3] == Approx(0.75)); REQUIRE(window[4] == Approx(1.0)); } SECTION("tick_sequence returns committed values at each step") { GoldenHarness h; h.eval_ok("(a1 0.75) (a2 a1)"); auto seq = h.tick_sequence("a2", {0.0, 0.001, 0.002}); REQUIRE(seq.size() == 3); REQUIRE(seq[0] == Approx(0.0)); // no prev yet REQUIRE(seq[1] == Approx(0.75)); // prev a1 is now 0.75 REQUIRE(seq[2] == Approx(0.75)); // still 0.75 } SECTION("verify_batch_matches_single for a compound expression") { GoldenHarness h; h.assign_ok("a1", "(+ (sin beat) (* 0.1 bar))"); h.verify_batch_matches_single("a1", 0.0, 1.0, 10); } SECTION("verify_batch_matches_single for constant expression") { GoldenHarness h; h.assign_ok("a1", "42"); h.verify_batch_matches_single("a1", 0.0, 2.0, 8); } } TEST_CASE("Golden semantics: additional coverage for spec gaps", "[golden][signal_engine][coverage]") { SECTION("variadic multiply folds correctly") { GoldenHarness h; h.assign_ok("a1", "(* 2 3 4)"); REQUIRE(h.sample("a1", 0.0) == Approx(24.0)); } SECTION("binary division") { GoldenHarness h; h.assign_ok("a1", "(/ 10 4)"); REQUIRE(h.sample("a1", 0.0) == Approx(2.5)); } SECTION("variadic division left folds") { GoldenHarness h; h.assign_ok("a1", "(/ 120 2 3)"); REQUIRE(h.sample("a1", 0.0) == Approx(20.0)); } SECTION("min selects minimum of two") { GoldenHarness h; h.assign_ok("a1", "(min 5 2)"); REQUIRE(h.sample("a1", 0.0) == Approx(2.0)); } SECTION("max selects maximum of two") { GoldenHarness h; h.assign_ok("a1", "(max 5 2)"); REQUIRE(h.sample("a1", 0.0) == Approx(5.0)); } SECTION("nil is falsy in if") { GoldenHarness h; h.assign_ok("a1", "(if 0 10 20)"); REQUIRE(h.sample("a1", 0.0) == Approx(20.0)); } SECTION("nested if picks correct branch") { GoldenHarness h; h.assign_ok("a1", "(if 1 (if 0 10 20) 30)"); REQUIRE(h.sample("a1", 0.0) == Approx(20.0)); } SECTION("let creates local scope") { GoldenHarness h; h.assign_ok("a1", "(let [x 10 y 20] (+ x y))"); REQUIRE(h.sample("a1", 0.0) == Approx(30.0)); } SECTION("data cell with time-varying lookup") { GoldenHarness h; h.eval_ok("(define notes [60 64 67 72])"); h.assign_ok("a1", "(interp notes beat)"); // At t=0.0, beat=0.0, scaled=0*3=0, interp = notes[0] = 60 REQUIRE(h.sample("a1", 0.0) == Approx(60.0)); // At t=0.25, beat=0.5, scaled=0.5*3=1.5, interp = 64 + 0.5*(67-64) = 65.5 REQUIRE(h.sample("a1", 0.25) == Approx(65.5)); } SECTION("diamond dependency: redefining root propagates through chain") { GoldenHarness h; h.eval_ok("(define root 1)"); h.eval_ok("(define mid (+ root 10))"); h.assign_ok("a1", "(+ mid 100)"); REQUIRE(h.sample("a1", 0.0) == Approx(111.0)); h.eval_ok("(define root 5)"); REQUIRE(h.sample("a1", 0.0) == Approx(115.0)); } SECTION("self-reference via prev for integration") { GoldenHarness h; h.eval_ok("(a1 (+ (prev a1) 0.1))"); auto seq = h.tick_sequence("a1", {0.0, 0.001, 0.002, 0.003}); REQUIRE(seq[0] == Approx(0.1)); // prev starts at 0 REQUIRE(seq[1] == Approx(0.2)); REQUIRE(seq[2] == Approx(0.3)); REQUIRE(seq[3] == Approx(0.4)); } SECTION("fast does not affect cells defined outside the warp") { GoldenHarness h; h.eval_ok("(define k 10)"); h.assign_ok("a1", "(fast 2 (+ k beat))"); // At t=0.125, fast-2 makes local t=0.25, beat=0.5, k=10 (unchanged) REQUIRE(h.sample("a1", 0.125) == Approx(10.5)); } SECTION("phrase phasor wraps at expected period") { GoldenHarness h(120.0, 4); h.assign_ok("a1", "phrase"); // At 120 bpm, 4/4, bars_per_phrase=4: one phrase = 4 bars = 8 seconds REQUIRE(h.sample("a1", 0.0) == Approx(0.0)); REQUIRE(h.sample("a1", 4.0) == Approx(0.5)); // At exactly 8.0 it wraps back to 0 REQUIRE(h.sample("a1", 8.0) == Approx(0.0).margin(1e-9)); } SECTION("section phasor wraps at expected period") { GoldenHarness h(120.0, 4); h.assign_ok("a1", "section"); // 4 phrases per section, each phrase = 8s, so section = 32s REQUIRE(h.sample("a1", 0.0) == Approx(0.0)); REQUIRE(h.sample("a1", 16.0) == Approx(0.5)); REQUIRE(h.sample("a1", 32.0) == Approx(0.0).margin(1e-9)); } SECTION("recursion rejection includes message about 'recursive'") { GoldenHarness h; h.eval_ok("(defn g [x] (g x))"); h.expect_error("(a1 (g 1))", DiagnosticCategory::Runtime, "recursive"); } SECTION("defs batch define works") { GoldenHarness h; h.eval_ok("(defs [x 10 y 20])"); h.assign_ok("a1", "(+ x y)"); REQUIRE(h.sample("a1", 0.0) == Approx(30.0)); } SECTION("fn anonymous lambda in signal context is rejected") { GoldenHarness h; h.expect_error("(a1 (fn [x] (* x 2)))", DiagnosticCategory::Boundary, "Lambda"); } SECTION("defn with multiple arguments") { GoldenHarness h; h.eval_ok("(defn add3 [a b c] (+ a b c))"); h.assign_ok("a1", "(add3 1 2 3)"); REQUIRE(h.sample("a1", 0.0) == Approx(6.0)); } SECTION("multiple outputs are independent") { GoldenHarness h; h.assign_ok("a1", "0.25"); h.assign_ok("a2", "0.75"); h.assign_ok("d1", "1.0"); REQUIRE(h.sample("a1", 0.0) == Approx(0.25)); REQUIRE(h.sample("a2", 0.0) == Approx(0.75)); REQUIRE(h.sample("d1", 0.0) == Approx(1.0)); } SECTION("sample_window over beat shows monotonic ramp") { GoldenHarness h; h.assign_ok("a1", "beat"); // Half a beat at 120 bpm = 0.25 seconds auto window = h.sample_window("a1", 0.0, 0.25, 5); REQUIRE(window.size() == 5); for (size_t i = 1; i < window.size(); ++i) { INFO("window[" << i-1 << "]=" << window[i-1] << " window[" << i << "]=" << window[i]); REQUIRE(window[i] > window[i-1]); } } } // ============================================================================ // Phase 2: Close core semantic spec gaps // ============================================================================ TEST_CASE("Golden semantics: variadic arithmetic edge cases (functions.md 1.7)", "[golden][signal_engine][variadic]") { SECTION("(+) nullary returns identity 0") { GoldenHarness h; h.assign_ok("a1", "(+)"); REQUIRE(h.sample("a1", 0.0) == Approx(0.0)); } SECTION("(*) nullary returns identity 1") { GoldenHarness h; h.assign_ok("a1", "(*)"); REQUIRE(h.sample("a1", 0.0) == Approx(1.0)); } SECTION("(/ x) unary division returns reciprocal (/ 1 x)") { GoldenHarness h; h.assign_ok("a1", "(/ 4)"); REQUIRE(h.sample("a1", 0.0) == Approx(0.25)); } SECTION("(- x) unary negation") { GoldenHarness h; h.assign_ok("a1", "(- 7)"); REQUIRE(h.sample("a1", 0.0) == Approx(-7.0)); } SECTION("variadic min with 3 args") { GoldenHarness h; h.assign_ok("a1", "(min 5 2 8)"); REQUIRE(h.sample("a1", 0.0) == Approx(2.0)); } SECTION("variadic min with 4 args") { GoldenHarness h; h.assign_ok("a1", "(min 10 3 7 1)"); REQUIRE(h.sample("a1", 0.0) == Approx(1.0)); } SECTION("variadic max with 3 args") { GoldenHarness h; h.assign_ok("a1", "(max 5 2 8)"); REQUIRE(h.sample("a1", 0.0) == Approx(8.0)); } SECTION("variadic max with 4 args") { GoldenHarness h; h.assign_ok("a1", "(max 1 9 3 6)"); REQUIRE(h.sample("a1", 0.0) == Approx(9.0)); } } TEST_CASE("Golden semantics: arity boundary errors", "[golden][signal_engine][arity]") { SECTION("(sin) with no args errors") { GoldenHarness h; h.expect_error("(a1 (sin))", DiagnosticCategory::Arity); } SECTION("(sin 1 2) with too many args errors") { GoldenHarness h; h.expect_error("(a1 (sin 1 2))", DiagnosticCategory::Arity); } SECTION("(clamp 1 2) missing third arg errors") { GoldenHarness h; h.expect_error("(a1 (clamp 1 2))", DiagnosticCategory::Arity); } SECTION("(step [1 2 3]) with no phasor defaults to beat (no error)") { GoldenHarness h; h.assign_ok("a1", "(step [1 2 3])"); REQUIRE(h.sample("a1", 0.0) == Approx(1.0)); } SECTION("(euclid 3) with only one arg errors") { GoldenHarness h; h.expect_error("(a1 (euclid 3))", DiagnosticCategory::Arity); } SECTION("error messages are plain language, not jargon") { GoldenHarness h; EvalResult r = h.eval_result("(a1 (sin))"); REQUIRE(r.kind == EvalResult::Error); REQUIRE(r.diagnostic_count > 0); std::string msg = r.diagnostics[0].message ? r.diagnostics[0].message : ""; INFO("message: " << msg); REQUIRE(msg.find("arity") == std::string::npos); } } TEST_CASE("Golden semantics: let/do/if edge cases", "[golden][signal_engine][control_flow]") { SECTION("(do expr1 expr2) returns last value") { GoldenHarness h; h.assign_ok("a1", "(do 1 2 3)"); REQUIRE(h.sample("a1", 0.0) == Approx(3.0)); } SECTION("(let [] 42) empty bindings returns body") { GoldenHarness h; h.assign_ok("a1", "(let [] 42)"); REQUIRE(h.sample("a1", 0.0) == Approx(42.0)); } SECTION("let bindings can reference earlier bindings (let* semantics)") { GoldenHarness h; h.assign_ok("a1", "(let [x 1 y (+ x 1)] y)"); REQUIRE(h.sample("a1", 0.0) == Approx(2.0)); } SECTION("if without else returns 0 for false condition") { GoldenHarness h; h.assign_ok("a1", "(if 0 42)"); REQUIRE(h.sample("a1", 0.0) == Approx(0.0)); } SECTION("if without else returns value for true condition") { GoldenHarness h; h.assign_ok("a1", "(if 1 42)"); REQUIRE(h.sample("a1", 0.0) == Approx(42.0)); } SECTION("nested let in signal context works") { GoldenHarness h; h.assign_ok("a1", "(let [x (+ beat 1)] (let [y (* x 2)] y))"); REQUIRE(h.sample("a1", 0.0) == Approx(2.0)); REQUIRE(h.sample("a1", 0.125) == Approx(2.5)); } SECTION("let with time-varying binding") { GoldenHarness h; h.assign_ok("a1", "(let [x beat] (* x 10))"); REQUIRE(h.sample("a1", 0.125) == Approx(2.5)); } SECTION("do with multiple expressions returns last") { GoldenHarness h; h.assign_ok("a1", "(do (+ 1 2) (* 3 4) (- 10 1))"); REQUIRE(h.sample("a1", 0.0) == Approx(9.0)); } } TEST_CASE("Golden semantics: sequence/index boundary cases", "[golden][signal_engine][sequences_boundary]") { SECTION("step with single-element vector returns constant") { GoldenHarness h; h.assign_ok("a1", "(step [42] beat)"); REQUIRE(h.sample("a1", 0.0) == Approx(42.0)); REQUIRE(h.sample("a1", 0.25) == Approx(42.0)); REQUIRE(h.sample("a1", 0.499) == Approx(42.0)); } SECTION("interp with single element returns that value") { GoldenHarness h; h.assign_ok("a1", "(interp [5] beat)"); REQUIRE(h.sample("a1", 0.0) == Approx(5.0)); REQUIRE(h.sample("a1", 0.25) == Approx(5.0)); } SECTION("from-list at exact boundaries") { GoldenHarness h; h.assign_ok("a1", "(from-list [10 20 30] beat)"); REQUIRE(h.sample("a1", 0.0) == Approx(10.0)); REQUIRE(h.sample("a1", 0.25) == Approx(20.0)); } SECTION("large vector step (32 elements)") { GoldenHarness h; h.assign_ok("a1", "(step [0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31] beat)"); REQUIRE(h.sample("a1", 0.0) == Approx(0.0)); REQUIRE(h.sample("a1", 0.25) == Approx(16.0)); } SECTION("step with empty vector returns zero") { GoldenHarness h; h.assign_ok("a1", "(step [] beat)"); REQUIRE(h.sample("a1", 0.0) == Approx(0.0)); } SECTION("gates with empty vector returns zero") { GoldenHarness h; h.assign_ok("a1", "(gates [] beat)"); REQUIRE(h.sample("a1", 0.0) == Approx(0.0)); } } TEST_CASE("Golden semantics: for unrolling (compilation.md 1.8)", "[golden][signal_engine][for_loop]") { SECTION("basic for returns last iteration value") { GoldenHarness h; h.assign_ok("a1", "(for x [1 2 3] x)"); REQUIRE(h.sample("a1", 0.0) == Approx(3.0)); } SECTION("for producing signal expression") { GoldenHarness h; h.assign_ok("a1", "(for x [1 2 3] (* x beat))"); REQUIRE(h.sample("a1", 0.125) == Approx(0.75)); } SECTION("for with accumulation via let") { GoldenHarness h; h.assign_ok("a1", "(for x [10 20 30] (+ x 5))"); REQUIRE(h.sample("a1", 0.0) == Approx(35.0)); } SECTION("for with empty collection returns 0") { GoldenHarness h; h.assign_ok("a1", "(for x [] x)"); REQUIRE(h.sample("a1", 0.0) == Approx(0.0)); } } TEST_CASE("Golden semantics: quote and symbol rejection", "[golden][signal_engine][quote]") { SECTION("(quote x) in signal context is rejected") { GoldenHarness h; h.expect_error("(a1 (quote x))", DiagnosticCategory::Boundary, "quote"); } } TEST_CASE("Golden semantics: alias semantics verification", "[golden][signal_engine][aliases]") { SECTION("from-list and seq produce identical results") { GoldenHarness h; h.assign_ok("a1", "(from-list [10 20 30] beat)"); h.assign_ok("a2", "(seq [10 20 30] beat)"); auto w1 = h.sample_window("a1", 0.0, 0.49, 10); auto w2 = h.sample_window("a2", 0.0, 0.49, 10); for (size_t i = 0; i < w1.size(); ++i) { INFO("i: " << i); REQUIRE(w1[i] == Approx(w2[i])); } } SECTION("trigs is binary (no width), gates has 50% duty cycle") { GoldenHarness h; h.assign_ok("a1", "(gates [1 1 1 1] beat)"); h.assign_ok("a2", "(trigs [1 1 1 1] beat)"); // trigs stays on for the whole step; gates turns off at 50% auto wg = h.sample_window("a1", 0.0, 0.49, 10); auto wt = h.sample_window("a2", 0.0, 0.49, 10); // At the start of a step both should be 1 REQUIRE(wg[0] == 1.0); REQUIRE(wt[0] == 1.0); // trigs should always be 1 for a non-zero pattern for (size_t i = 0; i < wt.size(); ++i) { REQUIRE(wt[i] == 1.0); } } SECTION("shift and offset produce identical results") { GoldenHarness h; h.assign_ok("a1", "(offset 0.1 beat)"); h.assign_ok("a2", "(shift 0.1 beat)"); auto w1 = h.sample_window("a1", 0.0, 0.49, 10); auto w2 = h.sample_window("a2", 0.0, 0.49, 10); for (size_t i = 0; i < w1.size(); ++i) { INFO("i: " << i); REQUIRE(w1[i] == Approx(w2[i])); } } SECTION("expt and pow have consistent but different arg order") { GoldenHarness h; h.assign_ok("a1", "(expt 2 10)"); REQUIRE(h.sample("a1", 0.0) == Approx(1024.0)); h.assign_ok("a2", "(pow 2 10)"); REQUIRE(h.sample("a2", 0.0) == Approx(100.0)); } } TEST_CASE("Golden semantics: non-numeric roots and string diagnostics", "[golden][signal_engine][type_errors]") { SECTION("string literal in output context produces error") { GoldenHarness h; EvalResult r = h.eval_result("(a1 \"hello\")"); REQUIRE(r.kind == EvalResult::Error); REQUIRE(r.diagnostic_count > 0); } } // ============================================================================ // Phase 3: Diagnostic Categories as First-Class Compiler Contract // ============================================================================ TEST_CASE("Diagnostics: UndefinedName category", "[golden][signal_engine][diagnostics][category]") { SECTION("unknown bare symbol produces UndefinedName") { GoldenHarness h; h.expect_error("(a1 nonexistent)", DiagnosticCategory::UndefinedName, "Unknown"); } SECTION("typo with fuzzy match produces UndefinedName with suggestion") { GoldenHarness h; h.eval_ok("(define freq 440)"); h.expect_error("(a1 frq)", DiagnosticCategory::UndefinedName, "freq", "freq"); } SECTION("typo sni suggests sin") { GoldenHarness h; h.expect_error("(a1 (sni beat))", DiagnosticCategory::UndefinedName, "sin", "sin"); } SECTION("totally unknown symbol still gets UndefinedName category") { GoldenHarness h; h.expect_error("(a1 zzzzzzz)", DiagnosticCategory::UndefinedName, "Unknown"); } SECTION("unknown function in call position produces UndefinedName") { GoldenHarness h; h.expect_error("(a1 (not-a-function 1))", DiagnosticCategory::UndefinedName); } SECTION("user-defined symbol typo gets fuzzy match") { GoldenHarness h; h.eval_ok("(define tempo 120)"); h.expect_error("(a1 tmpo)", DiagnosticCategory::UndefinedName, "tempo"); } } TEST_CASE("Diagnostics: Boundary category", "[golden][signal_engine][diagnostics][category]") { SECTION("define inside output is Boundary") { GoldenHarness h; h.expect_error("(a1 (define x 1))", DiagnosticCategory::Boundary, "inside an output"); } SECTION("defn inside output is Boundary") { GoldenHarness h; h.expect_error("(a1 (defn f [x] x))", DiagnosticCategory::Boundary, "inside an output"); } SECTION("lambda inside output is Boundary") { GoldenHarness h; h.expect_error("(a1 (fn [x] (* x 2)))", DiagnosticCategory::Boundary, "Lambda"); } SECTION("set inside output is Boundary") { GoldenHarness h; h.expect_error("(a1 (set x 1))", DiagnosticCategory::Boundary, "inside an output"); } SECTION("quote inside output is Boundary") { GoldenHarness h; h.expect_error("(a1 (quote (1 2 3)))", DiagnosticCategory::Boundary, "quote"); } } TEST_CASE("Diagnostics: Arity category", "[golden][signal_engine][diagnostics][category]") { SECTION("user function with too many args is Arity") { GoldenHarness h; h.eval_ok("(defn double [x] (* x 2))"); h.expect_error("(a1 (double 1 2))", DiagnosticCategory::Arity, "Too many"); } SECTION("user function with zero args when it needs one is Arity") { GoldenHarness h; h.eval_ok("(defn double [x] (* x 2))"); h.expect_error("(a1 (double))", DiagnosticCategory::Arity, "Wrong number"); } SECTION("bare function name (needs args) is Arity") { GoldenHarness h; h.eval_ok("(defn f [x] (* x 2))"); h.expect_error("(a1 f)", DiagnosticCategory::Arity, "needs arguments"); } } TEST_CASE("Diagnostics: Syntax category", "[golden][signal_engine][diagnostics][category]") { SECTION("non-symbol after open paren is Syntax") { GoldenHarness h; h.expect_error("(a1 (123 beat))", DiagnosticCategory::Syntax, "Expected a function name"); } SECTION("for with non-symbol variable is Syntax") { GoldenHarness h; h.expect_error("(a1 (for 123 [1 2 3] 1))", DiagnosticCategory::Syntax, "'for' needs a variable name"); } SECTION("unclosed form at EOF is Syntax") { GoldenHarness h; h.expect_error("(a1 (+ 1 2", DiagnosticCategory::Syntax, "Unclosed form"); } } TEST_CASE("Diagnostics: Type category", "[golden][signal_engine][diagnostics][category]") { SECTION("prev with non-output name is Type") { GoldenHarness h; h.expect_error("(a1 (prev beat))", DiagnosticCategory::Type, "output name"); } } TEST_CASE("Diagnostics: Runtime category (correct usage)", "[golden][signal_engine][diagnostics][category]") { SECTION("direct recursion is Runtime") { GoldenHarness h; h.eval_ok("(defn g [x] (g x))"); h.expect_error("(a1 (g 1))", DiagnosticCategory::Runtime, "recursive"); } SECTION("mutual recursion is Runtime") { GoldenHarness h; h.eval_ok("(define a (+ 1 b))"); h.eval_ok("(define b (+ 1 a))"); h.expect_error("(a1 a)", DiagnosticCategory::Runtime, "recursive"); } } // ============================================================================ // Span Accuracy Tests // ============================================================================ TEST_CASE("Diagnostics: span points at the correct subexpression", "[golden][signal_engine][diagnostics][span]") { SECTION("bare unknown symbol span") { GoldenHarness h; h.eval_ok("(define freq 440)"); // "(a1 frq)" — 'frq' starts at offset 4, length 3 h.expect_error_at("(a1 frq)", DiagnosticCategory::UndefinedName, 4, 3, "freq"); } SECTION("nested unknown symbol span") { GoldenHarness h; // "(a1 (+ 1 (sni beat)))" // 0123456789... // 'sni' starts at offset 10, length 3 h.expect_error_at("(a1 (+ 1 (sni beat)))", DiagnosticCategory::UndefinedName, 10, 3, "sin"); } SECTION("define inside output span points at define") { GoldenHarness h; // "(a1 (define x 1))" — 'define' starts at offset 5, length 6 h.expect_error_at("(a1 (define x 1))", DiagnosticCategory::Boundary, 5, 6, "inside an output"); } SECTION("unknown function in call position span") { GoldenHarness h; // "(a1 (blorg 1))" — 'blorg' starts at offset 5, length 5 h.expect_error_at("(a1 (blorg 1))", DiagnosticCategory::UndefinedName, 5, 5); } } // ============================================================================ // Fuzzy Matching Quality Tests // ============================================================================ TEST_CASE("Diagnostics: fuzzy match quality", "[golden][signal_engine][diagnostics][fuzzy]") { SECTION("distance 1 transposition: sni -> sin") { GoldenHarness h; h.expect_error("(a1 (sni beat))", DiagnosticCategory::UndefinedName, "sin", "sin"); } SECTION("distance 1 substitution: beet -> beat") { GoldenHarness h; h.expect_error("(a1 beet)", DiagnosticCategory::UndefinedName, "beat", "beat"); } SECTION("user-defined symbol fuzzy match: frq -> freq") { GoldenHarness h; h.eval_ok("(define freq 440)"); h.expect_error("(a1 frq)", DiagnosticCategory::UndefinedName, "freq", "freq"); } SECTION("no match for very different name — still UndefinedName") { GoldenHarness h; h.expect_error("(a1 zzzzzzz)", DiagnosticCategory::UndefinedName); } SECTION("function name fuzzy match in call position") { GoldenHarness h; // Should suggest 'sin' when 'sni' is used as a function EvalResult r = h.eval_result("(a1 (sni beat))"); REQUIRE(r.kind == EvalResult::Error); REQUIRE(r.diagnostic_count > 0); bool found_suggestion = false; for (uint8_t i = 0; i < r.diagnostic_count; ++i) { if (r.diagnostics[i].category == DiagnosticCategory::UndefinedName && r.diagnostics[i].suggestion != nullptr) { found_suggestion = true; REQUIRE(std::string(r.diagnostics[i].suggestion).find("sin") != std::string::npos); } } REQUIRE(found_suggestion); } } // ============================================================================ // Message Plainness Tests — no jargon // ============================================================================ TEST_CASE("Diagnostics: messages use plain language", "[golden][signal_engine][diagnostics][language]") { SECTION("arity error says 'Wrong number' not 'arity mismatch'") { GoldenHarness h; h.eval_ok("(defn f [x] (* x 2))"); EvalResult r = h.eval_result("(a1 (f 1 2))"); REQUIRE(r.kind == EvalResult::Error); REQUIRE(r.diagnostic_count > 0); std::string msg = r.diagnostics[0].message ? r.diagnostics[0].message : ""; // Must not contain jargon REQUIRE(msg.find("arity") == std::string::npos); REQUIRE(msg.find("predicate") == std::string::npos); REQUIRE(msg.find("lvalue") == std::string::npos); } SECTION("boundary error uses plain language") { GoldenHarness h; EvalResult r = h.eval_result("(a1 (define x 1))"); REQUIRE(r.kind == EvalResult::Error); REQUIRE(r.diagnostic_count > 0); std::string msg = r.diagnostics[0].message ? r.diagnostics[0].message : ""; REQUIRE(msg.find("side-effect") == std::string::npos); REQUIRE(msg.find("boundary violation") == std::string::npos); } SECTION("unknown name error uses plain language") { GoldenHarness h; EvalResult r = h.eval_result("(a1 nonexistent)"); REQUIRE(r.kind == EvalResult::Error); REQUIRE(r.diagnostic_count > 0); std::string msg = r.diagnostics[0].message ? r.diagnostics[0].message : ""; REQUIRE(msg.find("undefined variable") == std::string::npos); REQUIRE(msg.find("unbound") == std::string::npos); } } // ============================================================================ // Suggestion Validity Meta-Tests // ============================================================================ TEST_CASE("Diagnostics: suggestions compile successfully", "[golden][signal_engine][diagnostics][suggestion]") { SECTION("boundary error suggestion compiles") { GoldenHarness h; // The suggestion for define-inside-output is "Use it at the top level instead" // which is advice, not code — verify it's present EvalResult r = h.eval_result("(a1 (define x 1))"); REQUIRE(r.kind == EvalResult::Error); REQUIRE(r.diagnostic_count > 0); REQUIRE(r.diagnostics[0].suggestion != nullptr); } SECTION("fuzzy match suggestion compiles") { GoldenHarness h; h.eval_ok("(define freq 440)"); h.expect_suggestion_compiles("(a1 frq)", DiagnosticCategory::UndefinedName); } SECTION("lambda boundary suggestion is actionable") { GoldenHarness h; EvalResult r = h.eval_result("(a1 (fn [x] x))"); REQUIRE(r.kind == EvalResult::Error); REQUIRE(r.diagnostic_count > 0); REQUIRE(r.diagnostics[0].suggestion != nullptr); std::string sug = r.diagnostics[0].suggestion; // Should mention 'defn' REQUIRE(sug.find("defn") != std::string::npos); } SECTION("syntax error suggestion is present") { GoldenHarness h; EvalResult r = h.eval_result("(a1 (123 beat))"); REQUIRE(r.kind == EvalResult::Error); REQUIRE(r.diagnostic_count > 0); REQUIRE(r.diagnostics[0].suggestion != nullptr); } } // ============================================================================ // Multiple Diagnostics (recoverable errors) // ============================================================================ TEST_CASE("Diagnostics: error leaves previous output intact", "[golden][signal_engine][diagnostics][isolation]") { SECTION("compile error preserves last known good output") { GoldenHarness h; h.assign_ok("a1", "(+ beat 1)"); REQUIRE(h.sample("a1", 0.25) == Approx(1.5)); EvalResult bad = h.eval_result("(a1 (unknown-name beat))"); REQUIRE(bad.kind == EvalResult::Error); REQUIRE(bad.diagnostic_count > 0); REQUIRE(bad.diagnostics[0].category == DiagnosticCategory::UndefinedName); // Previous output still runs REQUIRE(h.sample("a1", 0.25) == Approx(1.5)); } SECTION("one output error does not affect another output") { GoldenHarness h; h.assign_ok("a1", "0.2"); h.assign_ok("a2", "0.8"); EvalResult bad = h.eval_result("(a1 (not-a-function 1))"); REQUIRE(bad.kind == EvalResult::Error); REQUIRE(bad.diagnostics[0].category == DiagnosticCategory::UndefinedName); REQUIRE(h.sample("a1", 0.0) == Approx(0.2)); REQUIRE(h.sample("a2", 0.0) == Approx(0.8)); } } // ============================================================================ // Comprehensive Category Coverage Audit // ============================================================================ TEST_CASE("Diagnostics: every category has at least one strict test", "[golden][signal_engine][diagnostics][coverage]") { SECTION("Syntax category coverage") { GoldenHarness h; // Non-symbol after open paren h.expect_error("(a1 (123 beat))", DiagnosticCategory::Syntax); } SECTION("UndefinedName category coverage") { GoldenHarness h; h.expect_error("(a1 nonexistent)", DiagnosticCategory::UndefinedName); } SECTION("Arity category coverage") { GoldenHarness h; h.eval_ok("(defn f [x] x)"); h.expect_error("(a1 (f 1 2))", DiagnosticCategory::Arity); } SECTION("Type category coverage") { GoldenHarness h; // prev with non-output argument h.expect_error("(a1 (prev beat))", DiagnosticCategory::Type); } SECTION("Boundary category coverage") { GoldenHarness h; h.expect_error("(a1 (define x 1))", DiagnosticCategory::Boundary); } SECTION("Runtime category coverage") { GoldenHarness h; h.eval_ok("(defn g [x] (g x))"); h.expect_error("(a1 (g 1))", DiagnosticCategory::Runtime); } } // ============================================================================ // Phase 5: Stateful Compiler Surface — defstate and integrate // ============================================================================ TEST_CASE("State: defstate creates a cell with initial value and update body", "[golden][signal_engine][state]") { SECTION("defstate counter increments each tick") { GoldenHarness h; h.eval_ok("(defstate counter 0 (+ counter 1))"); h.assign_ok("a1", "counter"); // First tick: counter should be 0 (initial value read before update) REQUIRE(h.tick("a1", 0.0) == Approx(0.0)); // After first tick, counter updates to 0+1=1 REQUIRE(h.tick("a1", 0.001) == Approx(1.0)); REQUIRE(h.tick("a1", 0.002) == Approx(2.0)); REQUIRE(h.tick("a1", 0.003) == Approx(3.0)); } SECTION("defstate with dt-based accumulation") { GoldenHarness h; h.eval_ok("(defstate accum 0 (+ accum dt))"); h.assign_ok("a1", "accum"); // First tick at t=0: accum=0 REQUIRE(h.tick("a1", 0.0) == Approx(0.0)); // dt = 0.001 - 0.0 = 0.001, accum = 0 + 0.001 REQUIRE(h.tick("a1", 0.001) == Approx(0.001)); // dt = 0.002 - 0.001 = 0.001, accum = 0.001 + 0.001 REQUIRE(h.tick("a1", 0.002) == Approx(0.002)); } SECTION("defstate update body can reference cells") { GoldenHarness h; h.eval_ok("(define rate 10)"); h.eval_ok("(defstate accum 0 (+ accum (* rate dt)))"); h.assign_ok("a1", "accum"); // accum grows at rate=10 per second REQUIRE(h.tick("a1", 0.0) == Approx(0.0)); // dt=0.001, accum = 0 + 10*0.001 = 0.01 REQUIRE(h.tick("a1", 0.001) == Approx(0.01)); // dt=0.001, accum = 0.01 + 10*0.001 = 0.02 REQUIRE(h.tick("a1", 0.002) == Approx(0.02)); } SECTION("defstate value persists across cell redefinition") { GoldenHarness h; h.eval_ok("(define rate 1)"); h.eval_ok("(defstate phase 0 (+ phase (* rate dt)))"); h.assign_ok("a1", "phase"); // Tick a few times at rate=1 REQUIRE(h.tick("a1", 0.0) == Approx(0.0)); REQUIRE(h.tick("a1", 0.001) == Approx(0.001)); REQUIRE(h.tick("a1", 0.002) == Approx(0.002)); double before = 0.002; // Redefine rate — phase's state should NOT reset h.eval_ok("(define rate 2)"); // dt=0.001, phase = 0.002 + 2*0.001 = 0.004 REQUIRE(h.tick("a1", 0.003) == Approx(before + 2.0 * 0.001)); } SECTION("defstate with non-zero initial value") { GoldenHarness h; h.eval_ok("(defstate x 100 (+ x 1))"); h.assign_ok("a1", "x"); REQUIRE(h.tick("a1", 0.0) == Approx(100.0)); REQUIRE(h.tick("a1", 0.001) == Approx(101.0)); REQUIRE(h.tick("a1", 0.002) == Approx(102.0)); } SECTION("multiple defstate cells are independent") { GoldenHarness h; h.eval_ok("(defstate a 0 (+ a 1))"); h.eval_ok("(defstate b 10 (+ b 2))"); h.assign_ok("a1", "a"); h.assign_ok("a2", "b"); REQUIRE(h.tick("a1", 0.0) == Approx(0.0)); REQUIRE(h.tick("a2", 0.0) == Approx(10.0)); // same tick via harness REQUIRE(h.tick("a1", 0.001) == Approx(1.0)); REQUIRE(h.tick("a2", 0.001) == Approx(12.0)); } SECTION("defstate inside output expression is rejected") { GoldenHarness h; h.expect_error("(a1 (defstate x 0 (+ x 1)))", DiagnosticCategory::Boundary); } } TEST_CASE("State: integrate accumulates rate over time", "[golden][signal_engine][state]") { SECTION("integrate constant rate approximates linear ramp") { GoldenHarness h; h.assign_ok("a1", "(integrate 1.0)"); // integrate(1.0) = state += 1.0 * dt per tick REQUIRE(h.tick("a1", 0.0) == Approx(0.0)); double dt = 0.001; REQUIRE(h.tick("a1", dt) == Approx(dt)); REQUIRE(h.tick("a1", 2*dt) == Approx(2*dt)); REQUIRE(h.tick("a1", 3*dt) == Approx(3*dt)); } SECTION("integrate with rate from cell") { GoldenHarness h; h.eval_ok("(define rate 5.0)"); h.assign_ok("a1", "(integrate rate)"); // state += 5.0 * dt per tick REQUIRE(h.tick("a1", 0.0) == Approx(0.0)); double dt = 0.001; REQUIRE(h.tick("a1", dt) == Approx(5.0 * dt)); REQUIRE(h.tick("a1", 2*dt) == Approx(2 * 5.0 * dt)); } SECTION("integrate with time-varying rate") { GoldenHarness h; h.assign_ok("a1", "(integrate t)"); // state += t * dt per tick (t changes) REQUIRE(h.tick("a1", 0.0) == Approx(0.0)); // dt=0.001, state = 0 + 0.001 * 0.001 = 0.000001 REQUIRE(h.tick("a1", 0.001) == Approx(0.001 * 0.001)); } SECTION("multiple independent integrates in same output") { GoldenHarness h; h.assign_ok("a1", "(+ (integrate 1.0) (integrate 2.0))"); // Two separate state slots, summed REQUIRE(h.tick("a1", 0.0) == Approx(0.0)); double dt = 0.001; REQUIRE(h.tick("a1", dt) == Approx(3.0 * dt)); REQUIRE(h.tick("a1", 2*dt) == Approx(2 * 3.0 * dt)); } } // ── Top-level expression eval (scratch isolation) ────────────────────────── TEST_CASE("Golden: top-level bar returns value at injected time", "[golden][scratch_eval]") { GoldenHarness h; // At 120 BPM, beat period = 0.5s. bar = fmod(t * bpm/60, 1) at t=0.125 // beat = fmod(0.125 * 2, 1) = 0.25 h.engine.state.current_time = 0.125; h.engine.state.current_dt = 0.0; EvalResult r = h.eval_result("bar"); REQUIRE(r.kind == EvalResult::Number); // bar = fmod(t * bpm / 60 / beats_per_bar, 1.0) = fmod(0.125*120/60/4, 1) = fmod(0.0625, 1) = 0.0625 REQUIRE(r.number == Approx(0.0625).margin(1e-9)); } TEST_CASE("Golden: top-level (* bar 0.5) returns number", "[golden][scratch_eval]") { GoldenHarness h; h.engine.state.current_time = 0.0; EvalResult r = h.eval_result("(* bar 0.5)"); REQUIRE(r.kind == EvalResult::Number); // At t=0, bar = 0, so (* 0 0.5) = 0 REQUIRE(r.number == Approx(0.0)); } TEST_CASE("Golden: top-level eval-at-time returns number", "[golden][scratch_eval]") { GoldenHarness h; h.engine.state.current_time = 0.0; EvalResult r = h.eval_result("(eval-at-time 0.5 bar)"); REQUIRE(r.kind == EvalResult::Number); // eval-at-time 0.5 bar: bar at t=0.5 at 120bpm 4/4 = fmod(0.5*120/60/4, 1) = fmod(0.25, 1) = 0.25 REQUIRE(r.number == Approx(0.25).margin(1e-9)); } TEST_CASE("Golden: top-level vector returns text with samples", "[golden][scratch_eval]") { GoldenHarness h; h.engine.state.current_time = 0.0; EvalResult r = h.eval_result("[(eval-at-time 0 bar) (eval-at-time 0.5 bar)]"); REQUIRE(r.kind == EvalResult::Text); // Should contain [0 0.25] or similar REQUIRE(r.text != nullptr); REQUIRE(r.text_length > 0); std::string text(r.text, r.text_length); REQUIRE(text[0] == '['); REQUIRE(text[text.size()-1] == ']'); } TEST_CASE("Golden: repeated top-level eval doesn't grow live node pool", "[golden][scratch_eval]") { GoldenHarness h; h.engine.state.current_time = 0.0; uint16_t initial_count = h.engine.pool.node_count; for (int i = 0; i < 10; i++) { EvalResult r = h.eval_result("(* bar 0.5)"); REQUIRE(r.kind == EvalResult::Number); } REQUIRE(h.engine.pool.node_count == initial_count); } TEST_CASE("Golden: repeated top-level vector eval doesn't grow live data tables", "[golden][scratch_eval]") { GoldenHarness h; h.engine.state.current_time = 0.0; uint8_t initial_tables = h.engine.cells.data_table_count; for (int i = 0; i < 5; i++) { EvalResult r = h.eval_result("[(eval-at-time 0 bar) (eval-at-time 0.5 bar)]"); REQUIRE(r.kind == EvalResult::Text); } REQUIRE(h.engine.cells.data_table_count == initial_tables); } TEST_CASE("Golden: invalid top-level signal expression returns error", "[golden][scratch_eval]") { GoldenHarness h; EvalResult r = h.eval_result("(nonexistent-function bar)"); REQUIRE(r.kind == EvalResult::Error); REQUIRE(r.diagnostic_count > 0); } TEST_CASE("Golden: defined cell visible in top-level expression", "[golden][scratch_eval]") { GoldenHarness h; h.eval_ok("(define freq 440)"); h.engine.state.current_time = 0.0; EvalResult r = h.eval_result("(* freq 2)"); REQUIRE(r.kind == EvalResult::Number); REQUIRE(r.number == Approx(880.0)); } // ── State Resource Registry ───────────────────────────────────────────────── TEST_CASE("Registry: resolve allocates new slot on first call", "[golden][registry]") { StateResourceRegistry reg; double state_values[MAX_STATE_SLOTS] = {}; uint16_t slot_count = 0; StateResourceKey key = { internSymbol("phase-A"), ResourceKind::OscillatorPhase, 0 }; uint16_t slot = reg.resolve(key, 0.0, state_values, slot_count); REQUIRE(slot == 0); REQUIRE(slot_count == 1); REQUIRE(reg.entry_count == 1); REQUIRE(state_values[0] == 0.0); } TEST_CASE("Registry: resolve returns existing slot for matching key", "[golden][registry]") { StateResourceRegistry reg; double state_values[MAX_STATE_SLOTS] = {}; uint16_t slot_count = 0; StateResourceKey key = { internSymbol("phase-A"), ResourceKind::OscillatorPhase, 0 }; uint16_t slot1 = reg.resolve(key, 0.0, state_values, slot_count); state_values[slot1] = 0.75; uint16_t slot2 = reg.resolve(key, 0.0, state_values, slot_count); REQUIRE(slot2 == slot1); REQUIRE(slot_count == 1); REQUIRE(state_values[slot2] == 0.75); } TEST_CASE("Registry: different keys get different slots", "[golden][registry]") { StateResourceRegistry reg; double state_values[MAX_STATE_SLOTS] = {}; uint16_t slot_count = 0; StateResourceKey k1 = { internSymbol("A"), ResourceKind::OscillatorPhase, 0 }; StateResourceKey k2 = { internSymbol("B"), ResourceKind::OscillatorPhase, 0 }; StateResourceKey k3 = { internSymbol("A"), ResourceKind::TriggerMemory, 0 }; uint16_t s1 = reg.resolve(k1, 0.0, state_values, slot_count); uint16_t s2 = reg.resolve(k2, 0.0, state_values, slot_count); uint16_t s3 = reg.resolve(k3, 1.0, state_values, slot_count); REQUIRE(s1 != s2); REQUIRE(s1 != s3); REQUIRE(s2 != s3); REQUIRE(slot_count == 3); REQUIRE(state_values[s3] == 1.0); } TEST_CASE("Registry: mark_all_inactive and re-resolve reactivates", "[golden][registry]") { StateResourceRegistry reg; double state_values[MAX_STATE_SLOTS] = {}; uint16_t slot_count = 0; StateResourceKey key = { internSymbol("X"), ResourceKind::Integrator, 0 }; reg.resolve(key, 0.0, state_values, slot_count); REQUIRE(reg.entries[0].active == true); reg.mark_all_inactive(); REQUIRE(reg.entries[0].active == false); reg.resolve(key, 0.0, state_values, slot_count); REQUIRE(reg.entries[0].active == true); REQUIRE(slot_count == 1); } TEST_CASE("Registry: clear resets completely", "[golden][registry]") { StateResourceRegistry reg; double state_values[MAX_STATE_SLOTS] = {}; uint16_t slot_count = 0; StateResourceKey key = { internSymbol("Y"), ResourceKind::SlewAccumulator, 0 }; reg.resolve(key, 0.5, state_values, slot_count); REQUIRE(reg.entry_count == 1); reg.clear(); REQUIRE(reg.entry_count == 0); } TEST_CASE("Golden: repeated set with expression doesn't grow live pool", "[golden][scratch_eval]") { GoldenHarness h; h.eval_ok("(define x 1)"); uint16_t initial_nodes = h.engine.pool.node_count; for (int i = 0; i < 10; i++) { h.eval_ok("(set x (+ 1 2))"); } REQUIRE(h.engine.pool.node_count == initial_nodes); EvalResult r = h.eval_result("x"); REQUIRE(r.kind == EvalResult::Number); REQUIRE(r.number == Approx(3.0)); }