ModuLisp/test/signal_engine/test_output_classification.cpp

168 lines
6.5 KiB
C++

// 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 <cstring>
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<OutputClass, uint32_t> 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);
}