// Vector-packing sugar for external sinks (NISPS-USEQ spec §4.3/§4.4): a // descriptor with vec_base_ch/vec_mod_ch set accepts its channel expressions // flat, as [base…] with optional :offset [mod…], or as [[base mod] scalar …]. // // Written at the language boundary like test_ext_registry.cpp: eval source // text, tick the engine, and assert the user-visible contract — identical // sink_values rows across forms, arity/exclusivity compile errors, per-sink // LKG across a failed sugar edit, and unchanged plain-sink behaviour. #define CATCH_CONFIG_MAIN #include "../catch.hpp" #include "src/signal_engine/signal_engine.h" #include #include #include using namespace sig; namespace { struct SugarHarness { SignalEngine engine; double cell_values[MAX_CELLS] = {}; double hw_inputs[32] = {}; double outputs[MAX_OUTPUTS] = {}; double workspace[MAX_TOTAL_NODES] = {}; SugarHarness() { engine.init_defaults(); reset_registry(); } ~SugarHarness() { reset_registry(); } EvalResult eval(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(code); INFO("code: " << code); if (r.kind == EvalResult::Error && r.diagnostic_count > 0) { INFO("diagnostic: " << (r.diagnostics[0].message ? r.diagnostics[0].message : "")); } REQUIRE(r.kind != EvalResult::Error); } void expect_error(const std::string& code, DiagnosticCategory category) { EvalResult r = eval(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 == category) found = true; INFO("expected category: " << category_to_cstr(category)); if (!found && r.diagnostics[0].message) { INFO("first diagnostic: " << r.diagnostics[0].message); } REQUIRE(found); } const SinkBinding* binding_for(const char* sink_name) { const SymbolID sink = internSymbol(sink_name); for (uint8_t i = 0; i < engine.sink_binding_count; i++) if (engine.sink_bindings[i].sink == sink) return &engine.sink_bindings[i]; return nullptr; } uint8_t binding_row(const char* sink_name) { const SymbolID sink = internSymbol(sink_name); for (uint8_t i = 0; i < engine.sink_binding_count; i++) if (engine.sink_bindings[i].sink == sink) return i; return MAX_SINK_BINDINGS; // sentinel: not bound } void tick_sinks(double t) { std::memset(outputs, 0, sizeof(outputs)); std::memset(workspace, 0, sizeof(workspace)); engine.cells.snapshot_values(cell_values, MAX_CELLS); ExecutionContext ctx; ctx.t = t; ctx.dt = 0.0; 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); publish_sink_values(engine, outputs); commit_outputs(engine.pool, outputs); } }; // One 8+8 packed sink named for what it tests; bases 0.1..0.8, mods // 0.01..0.08. Mirrors the nn/in firmware shape (spec §4.2/§4.3). constexpr float kBases[8] = {0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f}; constexpr float kMods[8] = {0.01f, 0.02f, 0.03f, 0.04f, 0.05f, 0.06f, 0.07f, 0.08f}; const char* kFlat = "(vec/in 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 " "0.01 0.02 0.03 0.04 0.05 0.06 0.07 0.08)"; const char* kOffset = "(vec/in [0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8] " ":offset [0.01 0.02 0.03 0.04 0.05 0.06 0.07 0.08])"; const char* kPairs = "(vec/in [[0.1 0.01] [0.2 0.02] [0.3 0.03] [0.4 0.04] " "[0.5 0.05] [0.6 0.06] [0.7 0.07] [0.8 0.08]])"; } // namespace // ── Descriptor validation (registration seam) ─────────────────────────────── TEST_CASE("vector-packing descriptor fields validate at registration", "[sink_sugar][ext_registry]") { SugarHarness h; SECTION("both zero (defaults) keeps today's plain sink") { REQUIRE(register_external_sink({"plain/out", 3, 0.0f, 1.0f, 50, 7})); const ExternalSinkDesc* desc = find_external_sink(internSymbol("plain/out")); REQUIRE(desc != nullptr); REQUIRE(desc->vec_base_ch == 0); REQUIRE(desc->vec_mod_ch == 0); } SECTION("base + mod channels must both be set and sum to arity") { REQUIRE(register_external_sink( {"vec/in", 16, 0.0f, 1.0f, 200, 7, 8, 8})); const ExternalSinkDesc* desc = find_external_sink(internSymbol("vec/in")); REQUIRE(desc != nullptr); REQUIRE(desc->vec_base_ch == 8); REQUIRE(desc->vec_mod_ch == 8); } SECTION("half-packed descriptors are rejected") { REQUIRE(!register_external_sink( {"bad/in", 16, 0.0f, 1.0f, 200, 7, 8, 0})); REQUIRE(!register_external_sink( {"bad/in", 16, 0.0f, 1.0f, 200, 7, 0, 8})); REQUIRE(external_sink_registered(internSymbol("bad/in")) == false); } SECTION("the split must cover the arity exactly") { REQUIRE(!register_external_sink( {"bad/in", 16, 0.0f, 1.0f, 200, 7, 7, 8})); // 15 of 16 REQUIRE(!register_external_sink( {"bad/in", 12, 0.0f, 1.0f, 200, 7, 8, 8})); // 16 of 12 REQUIRE(external_sink_registered(internSymbol("bad/in")) == false); } } // ── Accepted forms ────────────────────────────────────────────────────────── TEST_CASE("three forms of one binding publish identical sink rows", "[sink_sugar]") { SugarHarness h; REQUIRE(register_external_sink( {"vec/in", 16, 0.0f, 1.0f, 200, 7, 8, 8})); SECTION("(a) flat, :offset, and pair forms agree") { h.eval_ok(kFlat); h.tick_sinks(0.0); const SinkBinding* binding = h.binding_for("vec/in"); REQUIRE(binding != nullptr); REQUIRE(binding->arity == 16); double expected[16]; for (uint8_t ch = 0; ch < 16; ch++) expected[ch] = h.engine.sink_values[0][ch]; for (uint8_t i = 0; i < 8; i++) { REQUIRE(expected[i] == Approx(kBases[i])); REQUIRE(expected[8 + i] == Approx(kMods[i])); } h.eval_ok(kOffset); REQUIRE(h.engine.sink_binding_count == 1); h.tick_sinks(1.0); for (uint8_t ch = 0; ch < 16; ch++) REQUIRE(h.engine.sink_values[0][ch] == Approx(expected[ch])); h.eval_ok(kPairs); REQUIRE(h.engine.sink_binding_count == 1); h.tick_sinks(2.0); for (uint8_t ch = 0; ch < 16; ch++) REQUIRE(h.engine.sink_values[0][ch] == Approx(expected[ch])); // Every channel still occupies an ordinary sink pool slot. for (uint8_t ch = 0; ch < 16; ch++) { REQUIRE(binding->value_index[ch] >= SINK_SLOT_BASE); REQUIRE(binding->value_index[ch] < MAX_OUTPUTS); } } SECTION("(a2) a base-only vector defaults every modulation to zero") { h.eval_ok("(vec/in [0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8])"); h.tick_sinks(0.0); for (uint8_t i = 0; i < 8; i++) { REQUIRE(h.engine.sink_values[0][i] == Approx(kBases[i])); REQUIRE(h.engine.sink_values[0][8 + i] == Approx(0.0)); } } SECTION("(a3) a mixed pair vector binds scalar and paired entries") { h.eval_ok("(vec/in [[0.1 0.01] 0.2 0.3 [0.4 0.04] 0.5 0.6 0.7 0.8])"); h.tick_sinks(0.0); for (uint8_t i = 0; i < 8; i++) { REQUIRE(h.engine.sink_values[0][i] == Approx(kBases[i])); const float mod = (i == 0 || i == 3) ? kMods[i] : 0.0f; REQUIRE(h.engine.sink_values[0][8 + i] == Approx(mod)); } } } TEST_CASE("entries may be arbitrary expressions in every form", "[sink_sugar]") { SugarHarness h; REQUIRE(register_external_input({"ctl/x", 2, 1, 0.0f, "[0,1]"})); REQUIRE(register_external_sink( {"vec/in", 16, 0.0f, 1.0f, 200, 7, 8, 8})); h.hw_inputs[2] = 0.25; SECTION("(e1) flat form") { h.eval_ok("(vec/in (+ ctl/x 0.1) 0.2 0.3 0.4 0.5 0.6 0.7 0.8 " "(- ctl/x 0.05) 0.02 0.03 0.04 0.05 0.06 0.07 0.08)"); h.tick_sinks(0.0); REQUIRE(h.engine.sink_values[0][0] == Approx(0.35)); REQUIRE(h.engine.sink_values[0][8] == Approx(0.20)); } SECTION("(e2) :offset form, expressions in both vectors") { h.eval_ok("(vec/in [(+ ctl/x 0.1) 0.2 0.3 0.4 0.5 0.6 0.7 0.8] " ":offset [(- ctl/x 0.05) (* ctl/x 8) 0.03 0.04 " "0.05 0.06 0.07 0.08])"); h.tick_sinks(0.0); REQUIRE(h.engine.sink_values[0][0] == Approx(0.35)); REQUIRE(h.engine.sink_values[0][8] == Approx(0.20)); REQUIRE(h.engine.sink_values[0][9] == Approx(2.0)); } SECTION("(e3) pair form, expressions in both slots of a pair") { h.eval_ok("(vec/in [[(+ ctl/x 0.1) (- ctl/x 0.05)] " "[(* ctl/x 2) 0.5] 0.3 0.4 0.5 0.6 0.7 0.8])"); h.tick_sinks(0.0); REQUIRE(h.engine.sink_values[0][0] == Approx(0.35)); REQUIRE(h.engine.sink_values[0][8] == Approx(0.20)); REQUIRE(h.engine.sink_values[0][1] == Approx(0.5)); REQUIRE(h.engine.sink_values[0][9] == Approx(0.5)); // scalar entries still take the implicit zero modulation REQUIRE(h.engine.sink_values[0][10] == Approx(0.0)); } } // ── Rejected forms ────────────────────────────────────────────────────────── TEST_CASE("sugar arity errors are compile errors that bind nothing", "[sink_sugar][arity]") { SugarHarness h; REQUIRE(register_external_sink( {"vec/in", 16, 0.0f, 1.0f, 200, 7, 8, 8})); SECTION("(c1) base vector with 7 entries") { h.expect_error("(vec/in [0.1 0.2 0.3 0.4 0.5 0.6 0.7] " ":offset [0.01 0.02 0.03 0.04 0.05 0.06 0.07 0.08])", DiagnosticCategory::Arity); } SECTION("(c2) :offset vector with 9 entries") { h.expect_error("(vec/in [0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8] " ":offset [0.01 0.02 0.03 0.04 0.05 0.06 0.07 0.08 0.09])", DiagnosticCategory::Arity); } SECTION("(c3) pair entry with three entries") { h.expect_error("(vec/in [[0.1 0.01 0.9] 0.2 0.3 0.4 0.5 0.6 0.7 0.8])", DiagnosticCategory::Arity); } SECTION("(c3b) pair entry with one entry") { h.expect_error("(vec/in [[0.1] 0.2 0.3 0.4 0.5 0.6 0.7 0.8])", DiagnosticCategory::Arity); } SECTION("(c4) outer vector with 7 entries") { h.expect_error("(vec/in [0.1 0.2 0.3 0.4 0.5 0.6 0.7])", DiagnosticCategory::Arity); } SECTION("(c5) flat form with 15 expressions") { h.expect_error("(vec/in 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 " "0.01 0.02 0.03 0.04 0.05 0.06 0.07)", DiagnosticCategory::Arity); } SECTION("unknown keyword in the sink form") { h.expect_error("(vec/in [0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8] " ":modulation [0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1])", DiagnosticCategory::Syntax); } SECTION(":offset value that is not a vector") { h.expect_error("(vec/in [0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8] " ":offset 0.1)", DiagnosticCategory::Syntax); } SECTION("trailing value after the channel vector") { h.expect_error("(vec/in [0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8] 0.9)", DiagnosticCategory::Arity); } REQUIRE(h.binding_for("vec/in") == nullptr); REQUIRE(h.engine.sink_binding_count == 0); } TEST_CASE(":offset and pair entries are mutually exclusive", "[sink_sugar]") { SugarHarness h; REQUIRE(register_external_sink( {"vec/in", 16, 0.0f, 1.0f, 200, 7, 8, 8})); SECTION("(b1) a pair entry plus :offset") { h.expect_error("(vec/in [[0.1 0.01] 0.2 0.3 0.4 0.5 0.6 0.7 0.8] " ":offset [0.01 0.02 0.03 0.04 0.05 0.06 0.07 0.08])", DiagnosticCategory::Syntax); } SECTION("(b2) a nested pair inside the :offset vector") { h.expect_error("(vec/in [0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8] " ":offset [[0.01 0.02] 0.02 0.03 0.04 " "0.05 0.06 0.07 0.08])", DiagnosticCategory::Syntax); } REQUIRE(h.engine.sink_binding_count == 0); } // ── LKG and lifecycle ─────────────────────────────────────────────────────── TEST_CASE("a bad sugar edit keeps the prior binding while another sink stays " "live", "[sink_sugar][lkg]") { SugarHarness h; REQUIRE(register_external_input({"ctl/x", 2, 1, 0.0f, "[0,1]"})); REQUIRE(register_external_sink( {"vec/in", 16, 0.0f, 1.0f, 200, 7, 8, 8})); REQUIRE(register_external_sink({"transport/mono", 1, 0.0f, 1.0f, 50, 7})); h.eval_ok(kFlat); h.eval_ok("(transport/mono ctl/x)"); h.hw_inputs[2] = 0.5; h.tick_sinks(0.0); const uint8_t vec_row = h.binding_row("vec/in"); const uint8_t cc_row = h.binding_row("transport/mono"); REQUIRE(vec_row != MAX_SINK_BINDINGS); REQUIRE(cc_row != MAX_SINK_BINDINGS); double lkg[16]; for (uint8_t ch = 0; ch < 16; ch++) lkg[ch] = h.engine.sink_values[vec_row][ch]; // Exclusivity violation (spec §4.5): the whole form fails. h.expect_error("(vec/in [[0.9 0.01] 0.2 0.3 0.4 0.5 0.6 0.7 0.8] " ":offset [0.01 0.02 0.03 0.04 0.05 0.06 0.07 0.08])", DiagnosticCategory::Syntax); REQUIRE(h.engine.sink_binding_count == 2); // The failed edit retained vec/in's row; the other sink kept publishing. h.hw_inputs[2] = 0.75; h.tick_sinks(1.0); for (uint8_t ch = 0; ch < 16; ch++) REQUIRE(h.engine.sink_values[vec_row][ch] == Approx(lkg[ch])); REQUIRE(h.engine.sink_values[cc_row][0] == Approx(0.75)); // A corrected sugar edit then replaces the retained binding atomically. h.eval_ok("(vec/in [0.8 0.8 0.8 0.8 0.8 0.8 0.8 0.8])"); h.tick_sinks(2.0); for (uint8_t i = 0; i < 8; i++) { REQUIRE(h.engine.sink_values[vec_row][i] == Approx(0.8)); REQUIRE(h.engine.sink_values[vec_row][8 + i] == Approx(0.0)); } } TEST_CASE("unassign then rebind via sugar", "[sink_sugar]") { SugarHarness h; REQUIRE(register_external_sink( {"vec/in", 16, 0.0f, 1.0f, 200, 7, 8, 8})); h.eval_ok(kFlat); h.tick_sinks(0.0); REQUIRE(h.engine.sink_binding_count == 1); h.eval_ok("(unassign vec/in)"); REQUIRE(h.engine.sink_binding_count == 0); REQUIRE(h.binding_for("vec/in") == nullptr); h.eval_ok(kPairs); REQUIRE(h.engine.sink_binding_count == 1); const SinkBinding* binding = h.binding_for("vec/in"); REQUIRE(binding != nullptr); REQUIRE(binding->arity == 16); h.tick_sinks(1.0); for (uint8_t i = 0; i < 8; i++) { REQUIRE(h.engine.sink_values[0][i] == Approx(kBases[i])); REQUIRE(h.engine.sink_values[0][8 + i] == Approx(kMods[i])); } } // ── Plain sinks are untouched ─────────────────────────────────────────────── TEST_CASE("plain non-packed sinks reject the sugar forms", "[sink_sugar]") { SugarHarness h; REQUIRE(register_external_sink({"bus/out", 3, 0.0f, 1.0f, 50, 0})); // The sugar is descriptor-gated: a plain sink parses arguments as flat // expressions, so the sugar shapes fail as they always did — the // :offset keyword is an unknown name in expression position, and a // nested pair vector compiles to a plain vector expression (its // length), leaving too few flat arguments. h.expect_error("(bus/out [1 2] :offset [3 4])", DiagnosticCategory::UndefinedName); h.expect_error("(bus/out [[1 2] 3])", DiagnosticCategory::Arity); REQUIRE(h.engine.sink_binding_count == 0); // The flat form still binds exactly as before. h.eval_ok("(bus/out 0.1 0.2 0.3)"); h.tick_sinks(0.0); const SinkBinding* binding = h.binding_for("bus/out"); REQUIRE(binding != nullptr); REQUIRE(binding->arity == 3); REQUIRE(h.engine.sink_values[0][2] == Approx(0.3)); }