// Output Classification golden tests // Validates that the compiler correctly classifies outputs as // Pure, InputDep, or Stateful per visualisation.md §4. #define CATCH_CONFIG_MAIN #include "../catch.hpp" #include "src/signal_engine/signal_engine.h" #include using namespace sig; // Helper: compile an expression into output a1, return its classification static OutputClass classify(const char* expr) { SignalEngine engine; engine.init_defaults(); char wrapped[4096]; snprintf(wrapped, sizeof(wrapped), "(a1 %s)", expr); EvalResult r = eval_cold(wrapped, (uint32_t)strlen(wrapped), engine); REQUIRE(r.kind != EvalResult::Error); return engine.pool.output_class[0]; } // Helper: classify and also return the input mask static std::pair classify_with_mask(const char* expr) { SignalEngine engine; engine.init_defaults(); char wrapped[4096]; snprintf(wrapped, sizeof(wrapped), "(a1 %s)", expr); EvalResult r = eval_cold(wrapped, (uint32_t)strlen(wrapped), engine); REQUIRE(r.kind != EvalResult::Error); return { engine.pool.output_class[0], engine.pool.output_input_mask[0] }; } // ── Pure expressions ──────────────────────────────────────────────────────── TEST_CASE("Classification: constant is pure", "[classification]") { REQUIRE(classify("0.5") == OutputClass::Pure); } TEST_CASE("Classification: arithmetic on constants is pure", "[classification]") { REQUIRE(classify("(+ 1 2)") == OutputClass::Pure); } TEST_CASE("Classification: function of t is pure", "[classification]") { REQUIRE(classify("(sin (* t 2))") == OutputClass::Pure); } TEST_CASE("Classification: sin of beat (time-derived) is pure", "[classification]") { REQUIRE(classify("(sin beat)") == OutputClass::Pure); } TEST_CASE("Classification: nested time transforms are pure", "[classification]") { REQUIRE(classify("(fast 2 (sin beat))") == OutputClass::Pure); } TEST_CASE("Classification: seq of constants is pure", "[classification]") { REQUIRE(classify("(seq [0.2 0.5 0.8])") == OutputClass::Pure); } TEST_CASE("Classification: euclid pattern is pure", "[classification]") { REQUIRE(classify("(euclid 3 8 beat)") == OutputClass::Pure); } TEST_CASE("Classification: step with data table is pure", "[classification]") { REQUIRE(classify("(step [0.2 0.5 0.8])") == OutputClass::Pure); } TEST_CASE("Classification: if with pure branches is pure", "[classification]") { REQUIRE(classify("(if (> beat 0.5) 1.0 0.0)") == OutputClass::Pure); } // ── Input-dependent expressions ───────────────────────────────────────────── TEST_CASE("Classification: hardware input is input-dep", "[classification]") { auto [cls, mask] = classify_with_mask("ain1"); REQUIRE(cls == OutputClass::InputDep); REQUIRE((mask & (1u << 8)) != 0); // ain1 = INP_AI1 (input channel 8) } TEST_CASE("Classification: expression using hardware input is input-dep", "[classification]") { auto [cls, mask] = classify_with_mask("(* ain1 (sin beat))"); REQUIRE(cls == OutputClass::InputDep); } TEST_CASE("Classification: multiple inputs tracked in mask", "[classification]") { auto [cls, mask] = classify_with_mask("(+ ain1 ain2)"); REQUIRE(cls == OutputClass::InputDep); REQUIRE((mask & 0x300u) == 0x300u); // ain1=INP_AI1(ch8) + ain2=INP_AI2(ch9) → bits 8,9 } TEST_CASE("Classification: if with input condition is input-dep", "[classification]") { auto [cls, mask] = classify_with_mask("(if (> ain1 0.5) 1.0 0.0)"); REQUIRE(cls == OutputClass::InputDep); } // ── Stateful expressions ──────────────────────────────────────────────────── TEST_CASE("Classification: integrate is stateful", "[classification]") { REQUIRE(classify("(integrate 0.1)") == OutputClass::Stateful); } TEST_CASE("Classification: expression using prev is stateful", "[classification]") { REQUIRE(classify("(prev a1)") == OutputClass::Stateful); } // ── Multi-output classification ───────────────────────────────────────────── TEST_CASE("Classification: multiple outputs classified independently", "[classification]") { SignalEngine engine; engine.init_defaults(); // a1 = pure (sin of t) const char* src1 = "(a1 (sin beat))"; eval_cold(src1, (uint32_t)strlen(src1), engine); // a2 = input-dep const char* src2 = "(a2 ain1)"; eval_cold(src2, (uint32_t)strlen(src2), engine); // a3 = stateful const char* src3 = "(a3 (integrate 0.01))"; eval_cold(src3, (uint32_t)strlen(src3), engine); REQUIRE(engine.pool.output_class[0] == OutputClass::Pure); REQUIRE(engine.pool.output_class[1] == OutputClass::InputDep); REQUIRE(engine.pool.output_class[2] == OutputClass::Stateful); } // ── Inactive outputs ──────────────────────────────────────────────────────── TEST_CASE("Classification: unassigned output is inactive", "[classification]") { SignalEngine engine; engine.init_defaults(); // Only assign a1, leave a2 unassigned const char* src = "(a1 0.5)"; eval_cold(src, (uint32_t)strlen(src), engine); REQUIRE(engine.pool.output_class[0] == OutputClass::Pure); REQUIRE(engine.pool.output_class[1] == OutputClass::Inactive); } // ── Reclassification on recompile ─────────────────────────────────────────── TEST_CASE("Classification: reclassifies when expression changes", "[classification]") { SignalEngine engine; engine.init_defaults(); // Start pure const char* src1 = "(a1 (sin beat))"; eval_cold(src1, (uint32_t)strlen(src1), engine); REQUIRE(engine.pool.output_class[0] == OutputClass::Pure); // Change to input-dep const char* src2 = "(a1 ain1)"; eval_cold(src2, (uint32_t)strlen(src2), engine); REQUIRE(engine.pool.output_class[0] == OutputClass::InputDep); // Change to stateful const char* src3 = "(a1 (integrate 0.1))"; eval_cold(src3, (uint32_t)strlen(src3), engine); REQUIRE(engine.pool.output_class[0] == OutputClass::Stateful); }