feat(signal-engine): 16-channel sink arity, pool capacity 280, ColdArg::vec_len, Arduino-safe kTwoPi rename
This commit is contained in:
parent
db2031ae9e
commit
1b87f89121
13 changed files with 95 additions and 47 deletions
|
|
@ -160,7 +160,7 @@ static void publish_output_graph_plan(SignalEngine& engine,
|
|||
const GraphBuildResult& result) {
|
||||
engine.pool.outputs[output_index].root_node = result.root_node;
|
||||
engine.pool.outputs[output_index].valid = true;
|
||||
engine.pool.runtime_fallback_mask &= ~((uint64_t)1 << output_index);
|
||||
engine.pool.runtime_fallback[output_index] = 0;
|
||||
engine.output_compile_diagnostics[output_index] =
|
||||
ActiveCompileDiagnostic{};
|
||||
engine.pool.output_deps[output_index].clear();
|
||||
|
|
@ -2231,8 +2231,7 @@ static EvalResult do_unassign(TokenStream& ts, SignalEngine& engine) {
|
|||
engine.pool.prev_output_values[output_index] = 0.0;
|
||||
engine.pool.output_deps[output_index].clear();
|
||||
engine.output_sources[output_index] = OutputSource{};
|
||||
engine.pool.runtime_fallback_mask &=
|
||||
~((uint64_t)1 << output_index);
|
||||
engine.pool.runtime_fallback[output_index] = 0;
|
||||
engine.output_compile_diagnostics[output_index] =
|
||||
ActiveCompileDiagnostic{};
|
||||
|
||||
|
|
@ -2362,7 +2361,7 @@ static void release_output_slot(SignalEngine& engine, uint16_t slot) {
|
|||
engine.pool.outputs[slot] = OutputSlot{};
|
||||
engine.pool.prev_output_values[slot] = 0.0;
|
||||
engine.pool.output_deps[slot].clear();
|
||||
engine.pool.runtime_fallback_mask &= ~((uint64_t)1 << slot);
|
||||
engine.pool.runtime_fallback[slot] = 0;
|
||||
engine.output_compile_diagnostics[slot] = ActiveCompileDiagnostic{};
|
||||
engine.registry.begin_context(slot);
|
||||
engine.registry.commit_context(slot,
|
||||
|
|
@ -2550,10 +2549,11 @@ static EvalResult do_cold_command(const ExternalCommandDesc* desc,
|
|||
if (!parse_numeric_vector(ts, values, count, error)) return error;
|
||||
if (count > ColdArg::MAX_VEC) {
|
||||
return make_cold_arity_error(
|
||||
"Vector command arguments support at most 8 values",
|
||||
"Vector command arguments support at most 16 values",
|
||||
"Shorten the vector");
|
||||
}
|
||||
arg.kind = ColdArg::Kind::Vector;
|
||||
arg.vec_len = (uint8_t)count;
|
||||
for (uint16_t v = 0; v < count; v++)
|
||||
arg.vec[v] = (float)values[v];
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ OutputHealth output_health(const NodePool& pool, uint16_t output_index) {
|
|||
if (output_index >= MAX_OUTPUTS) return OutputHealth::Idle;
|
||||
const OutputSlot& slot = pool.outputs[output_index];
|
||||
if (!slot.valid || slot.root_node == NODE_NONE) return OutputHealth::Idle;
|
||||
if (((pool.runtime_fallback_mask >> output_index) & 1u) == 0)
|
||||
if (!pool.runtime_fallback_bit(output_index))
|
||||
return OutputHealth::Running;
|
||||
return slot.has_lkg ? OutputHealth::Fallback : OutputHealth::Error;
|
||||
}
|
||||
|
|
@ -176,9 +176,10 @@ void execute_all_outputs(const NodePool& pool, ExecutionContext& ctx) {
|
|||
ctx.workspace[idx] = result;
|
||||
}
|
||||
|
||||
// Read output values with LKG fallback
|
||||
uint64_t fallback_mask = 0;
|
||||
// Read output values with LKG fallback. runtime_fallback[] is recomputed
|
||||
// wholesale on every pass.
|
||||
for (uint16_t i = 0; i < MAX_OUTPUTS; i++) {
|
||||
pool.runtime_fallback[i] = 0;
|
||||
if (pool.outputs[i].root_node != NODE_NONE) {
|
||||
Sample v = ctx.workspace[pool.outputs[i].root_node];
|
||||
if (g_failure_mode == FailureMode::LkgFallback &&
|
||||
|
|
@ -187,7 +188,7 @@ void execute_all_outputs(const NodePool& pool, ExecutionContext& ctx) {
|
|||
// value (or the neutral default when no LKG exists —
|
||||
// failure-model.md §2.4) and record the fallback.
|
||||
v = pool.outputs[i].has_lkg ? pool.outputs[i].lkg_value : 0.0;
|
||||
fallback_mask |= (uint64_t)1 << i;
|
||||
pool.runtime_fallback[i] = 1;
|
||||
}
|
||||
ctx.output_values[i] = v;
|
||||
} else if (pool.outputs[i].valid) {
|
||||
|
|
@ -199,7 +200,6 @@ void execute_all_outputs(const NodePool& pool, ExecutionContext& ctx) {
|
|||
ctx.output_values[i] = 0.0;
|
||||
}
|
||||
}
|
||||
pool.runtime_fallback_mask = fallback_mask;
|
||||
}
|
||||
|
||||
// ── External Sink Publication ───────────────────────────────────────────────
|
||||
|
|
@ -237,8 +237,7 @@ void publish_sink_values(SignalEngine& engine, const Sample* output_values) {
|
|||
void commit_outputs(NodePool& pool, const Sample* output_values) {
|
||||
for (uint16_t i = 0; i < MAX_OUTPUTS; i++) {
|
||||
pool.prev_output_values[i] = output_values[i];
|
||||
bool substituted =
|
||||
((pool.runtime_fallback_mask >> i) & 1u) != 0;
|
||||
bool substituted = pool.runtime_fallback_bit(i);
|
||||
if (pool.outputs[i].root_node != NODE_NONE && !substituted &&
|
||||
std::isfinite(output_values[i])) {
|
||||
pool.outputs[i].lkg_value = output_values[i];
|
||||
|
|
@ -257,7 +256,7 @@ void commit_state(NodePool& pool, const Sample* workspace,
|
|||
if (pool.state_update_roots[s] != NODE_NONE) {
|
||||
uint16_t owner = pool.state_owner_context[s];
|
||||
if (owner < MAX_OUTPUTS &&
|
||||
((pool.runtime_fallback_mask >> owner) & 1u) != 0) {
|
||||
pool.runtime_fallback_bit(owner)) {
|
||||
// Stateful nodes owned by an output advance only when that
|
||||
// output publishes a healthy sample. Otherwise an oscillator
|
||||
// or integrator could run invisibly behind scalar fallback,
|
||||
|
|
@ -311,7 +310,7 @@ void execute_batch(
|
|||
|
||||
const size_t CHUNK = pool.batch_chunk_size;
|
||||
Sample* regs = pool.batch_workspace.get();
|
||||
uint64_t fallback_mask = 0;
|
||||
memset(pool.runtime_fallback, 0, sizeof(pool.runtime_fallback));
|
||||
|
||||
for (size_t chunk_start = 0; chunk_start < sample_count; chunk_start += CHUNK) {
|
||||
size_t chunk_size = std::min(CHUNK, sample_count - chunk_start);
|
||||
|
|
@ -385,7 +384,7 @@ void execute_batch(
|
|||
Sample v = src[s];
|
||||
if (!std::isfinite(v)) {
|
||||
v = lkg;
|
||||
fallback_mask |= (uint64_t)1 << o;
|
||||
pool.runtime_fallback[o] = 1;
|
||||
}
|
||||
dst[s] = v;
|
||||
}
|
||||
|
|
@ -400,7 +399,6 @@ void execute_batch(
|
|||
}
|
||||
}
|
||||
}
|
||||
pool.runtime_fallback_mask = fallback_mask;
|
||||
}
|
||||
|
||||
// ── Output Classification ───────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ namespace sig {
|
|||
// LkgFallback (default, spec-mandated): values propagate freely through
|
||||
// the node graph; a non-finite value reaching an OUTPUT ROOT makes that
|
||||
// output substitute its last-known-good value (or 0 if none) and marks
|
||||
// it as being in fallback (see NodePool::runtime_fallback_mask).
|
||||
// it as being in fallback (see NodePool::runtime_fallback).
|
||||
// ZeroSquash (legacy): every node's result is clamped to 0.0 when
|
||||
// non-finite. No fallback, no diagnostic — pre-v1.2 behaviour.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -18,8 +18,12 @@ namespace sig {
|
|||
// (useq-clear) and is deliberately NOT touched by SignalEngine session resets.
|
||||
// reset_registry() exists for tests and explicit profile re-initialisation.
|
||||
|
||||
constexpr uint8_t MAX_EXTERNAL_INPUTS = 24;
|
||||
constexpr uint8_t MAX_EXTERNAL_SINKS = 16;
|
||||
// Registry capacities sized for the uSEQ+NISPS firmware profile plus headroom:
|
||||
// 19 meml/* control registers + 8 nn/out* neural outputs = 27 inputs (cap 32,
|
||||
// aligned with MAX_HW_INPUT_CHANNELS); 32 midi/cc* transports + nn/in = 33
|
||||
// sinks (cap 40); 14 nn/* commands (cap 16).
|
||||
constexpr uint8_t MAX_EXTERNAL_INPUTS = 32;
|
||||
constexpr uint8_t MAX_EXTERNAL_SINKS = 40;
|
||||
constexpr uint8_t MAX_EXTERNAL_COMMANDS = 16;
|
||||
|
||||
// Width of the executor-side hw_inputs[] snapshot array (see
|
||||
|
|
@ -83,14 +87,20 @@ const ExternalCommandDesc* find_external_command(SymbolID cmd);
|
|||
// SINK_SLOT_BASE..MAX_OUTPUTS-1) so execution order, LKG fallback, and GC
|
||||
// reachability are the ordinary output machinery — no second mechanism.
|
||||
|
||||
constexpr uint8_t MAX_SINK_ARITY = 8;
|
||||
// 16 channel expressions per binding: the nn/in neural-input sink binds
|
||||
// 8 base + 8 modulation expressions in one form (NISPS-USEQ spec §4.2/§4.3).
|
||||
constexpr uint8_t MAX_SINK_ARITY = 16;
|
||||
constexpr uint8_t MAX_SINK_BINDINGS = 16;
|
||||
|
||||
// Pool output slots 0..23 are the named a/d/s outputs; sink channel graphs
|
||||
// occupy the remainder.
|
||||
// occupy the remainder. The pool must hold every binding row at full arity
|
||||
// simultaneously: 24 + 16 × 16 = 280 = MAX_OUTPUTS.
|
||||
constexpr uint16_t SINK_SLOT_BASE = 24;
|
||||
static_assert(SINK_SLOT_BASE < MAX_OUTPUTS,
|
||||
"sink channel slots must fit inside the pool output table");
|
||||
static_assert(SINK_SLOT_BASE + (uint32_t)MAX_SINK_BINDINGS * MAX_SINK_ARITY
|
||||
<= MAX_OUTPUTS,
|
||||
"pool outputs must fit every sink binding at full arity");
|
||||
|
||||
struct SinkBinding {
|
||||
SymbolID sink = SymbolIntern::INVALID_ID;
|
||||
|
|
@ -111,10 +121,12 @@ struct ColdArg {
|
|||
static constexpr uint8_t MAX_VEC = MAX_SINK_ARITY;
|
||||
|
||||
Kind kind = Kind::Int;
|
||||
// Number of entries in vec[] when kind == Vector (0..MAX_VEC). Without
|
||||
// it a handler could not validate an exact-width vector argument such as
|
||||
// nn/add-example's Vector[8] pair, nor tell a short vector from padding.
|
||||
uint8_t vec_len = 0;
|
||||
|
||||
// Number tokens without a fractional part that fit in int32 parse as
|
||||
// Int; everything numeric else is Number. Names parse as their interned
|
||||
// SymbolID; bracketed numeric literals parse as Vector.
|
||||
ColdArg() : integer(0) {}
|
||||
union {
|
||||
float number;
|
||||
int32_t integer;
|
||||
|
|
@ -122,7 +134,6 @@ struct ColdArg {
|
|||
float vec[MAX_VEC];
|
||||
};
|
||||
|
||||
ColdArg() : integer(0) {}
|
||||
};
|
||||
|
||||
using ColdCommandHandler = bool (*)(SymbolID cmd, const ColdArg* args,
|
||||
|
|
|
|||
|
|
@ -1076,7 +1076,7 @@ uint16_t GraphBuilder::compile_waveform(
|
|||
"Remove the extra arguments");
|
||||
}
|
||||
|
||||
constexpr Sample TWO_PI = 6.28318530717958647692;
|
||||
constexpr Sample kTwoPi = 6.28318530717958647692;
|
||||
const bool is_trig = waveform == sym.sin_ || waveform == sym.cos_ ||
|
||||
waveform == sym.bsin || waveform == sym.bcos ||
|
||||
waveform == sym.tan_;
|
||||
|
|
@ -1088,11 +1088,11 @@ uint16_t GraphBuilder::compile_waveform(
|
|||
if (name_space != OperatorNamespace::Radians &&
|
||||
name_space != OperatorNamespace::Raw) {
|
||||
phase_or_angle = pool.make_binop(
|
||||
NodeOp::Mul, input, pool.make_const(TWO_PI));
|
||||
NodeOp::Mul, input, pool.make_const(kTwoPi));
|
||||
}
|
||||
} else if (name_space == OperatorNamespace::Radians) {
|
||||
phase_or_angle = pool.make_binop(
|
||||
NodeOp::Div, input, pool.make_const(TWO_PI));
|
||||
NodeOp::Div, input, pool.make_const(kTwoPi));
|
||||
}
|
||||
|
||||
uint16_t result = NODE_NONE;
|
||||
|
|
|
|||
|
|
@ -487,7 +487,7 @@ void NodePool::reset() {
|
|||
memset(output_class, 0, sizeof(output_class));
|
||||
memset(output_input_mask, 0, sizeof(output_input_mask));
|
||||
memset(prev_output_values, 0, sizeof(prev_output_values));
|
||||
runtime_fallback_mask = 0;
|
||||
memset(runtime_fallback, 0, sizeof(runtime_fallback));
|
||||
state_update_failure_mask = 0;
|
||||
memset(state_values, 0, sizeof(state_values));
|
||||
for (uint16_t s = 0; s < MAX_STATE_SLOTS; s++) {
|
||||
|
|
|
|||
|
|
@ -145,13 +145,18 @@ struct NodePool {
|
|||
// Cross-output reads use previous-tick values
|
||||
Sample prev_output_values[MAX_OUTPUTS] = {};
|
||||
|
||||
// Runtime fallback tracking (failure-model.md §2.1/§5): bit i is set when
|
||||
// output i substituted its LKG value on the most recent execution pass
|
||||
// because a non-finite value reached its root (FailureMode::LkgFallback
|
||||
// Runtime fallback tracking (failure-model.md §2.1/§5): entry i is set
|
||||
// when output i substituted its LKG value on the most recent execution
|
||||
// pass because a non-finite value reached its root (FailureMode::LkgFallback
|
||||
// only). Recomputed on every pass; mutable because execution paths take
|
||||
// `const NodePool&` — this is diagnostic bookkeeping, not graph state.
|
||||
mutable uint64_t runtime_fallback_mask = 0;
|
||||
static_assert(MAX_OUTPUTS <= 64, "runtime_fallback_mask is 64-bit");
|
||||
// One byte per output rather than a uint64_t bitmask: the pool output
|
||||
// table grew past 64 entries when sink channel slots were sized for
|
||||
// 16 bindings × 16 channels (ext_registry.h).
|
||||
mutable uint8_t runtime_fallback[MAX_OUTPUTS] = {};
|
||||
bool runtime_fallback_bit(uint16_t output_index) const {
|
||||
return output_index < MAX_OUTPUTS && runtime_fallback[output_index] != 0;
|
||||
}
|
||||
|
||||
// Bit s is active when state slot s most recently produced a non-finite
|
||||
// update candidate. The previous finite state remains installed until
|
||||
|
|
|
|||
|
|
@ -67,7 +67,13 @@ constexpr size_t MAX_STATE_SLOTS = 32;
|
|||
#endif
|
||||
|
||||
constexpr size_t MAX_CALLABLE_PARAMS = 8;
|
||||
constexpr size_t MAX_OUTPUTS = 42;
|
||||
// Named a/d/s outputs occupy 0..SINK_SLOT_BASE-1; bound external-sink channel
|
||||
// graphs take pool slots above that (ext_registry.h). Sized so every binding
|
||||
// row can hold a full-arity sink at once: SINK_SLOT_BASE(24) +
|
||||
// MAX_SINK_BINDINGS(16) × MAX_SINK_ARITY(16) = 280 (static_assert beside the
|
||||
// constants in ext_registry.h). The nn/in neural-input sink needs 16 channels
|
||||
// (8 bases + 8 modulations, NISPS-USEQ spec §4.2/§4.3).
|
||||
constexpr size_t MAX_OUTPUTS = 280;
|
||||
constexpr size_t MAX_SCOPE_DEPTH = 32;
|
||||
constexpr size_t MAX_LOCAL_BINDINGS = 32;
|
||||
constexpr size_t MAX_DIAGNOSTICS = 16;
|
||||
|
|
|
|||
|
|
@ -259,6 +259,35 @@ TEST_CASE("external sinks publish evaluated values", "[ext_registry]")
|
|||
REQUIRE(h.engine.sink_values[0][2] == Approx(0.6));
|
||||
}
|
||||
|
||||
SECTION("full 16-channel sink binds and publishes every channel")
|
||||
{
|
||||
// The nn/in neural-input shape (NISPS-USEQ spec §4.2/§4.3): 8 base +
|
||||
// 8 modulation expressions in one binding. MAX_SINK_ARITY and the
|
||||
// pool arithmetic in ext_registry.h are sized for this.
|
||||
REQUIRE(register_external_sink({"nn/in", 16, 0.0f, 1.0f, 200, 7}));
|
||||
h.eval_ok("(nn/in 0.00 0.05 0.10 0.15 0.20 0.25 0.30 0.35 "
|
||||
"0.5 0.4 0.3 0.2 0.1 0.0 -0.1 -0.2)");
|
||||
h.tick_sinks(0.0);
|
||||
const SinkBinding* binding = h.binding_for("nn/in");
|
||||
REQUIRE(binding != nullptr);
|
||||
REQUIRE(binding->arity == 16);
|
||||
for (uint8_t ch = 0; ch < 16; ch++) {
|
||||
const double expected = ch < 8 ? 0.05 * ch : 0.5 - 0.1 * (ch - 8);
|
||||
REQUIRE(h.engine.sink_values[0][ch] == Approx(expected));
|
||||
}
|
||||
// Slot plan: 16 pool output slots at/above SINK_SLOT_BASE.
|
||||
for (uint8_t ch = 0; ch < 16; ch++) {
|
||||
REQUIRE(binding->value_index[ch] >= SINK_SLOT_BASE);
|
||||
REQUIRE(binding->value_index[ch] < MAX_OUTPUTS);
|
||||
}
|
||||
|
||||
// Rebinding swaps all 16 channels as one transaction.
|
||||
h.eval_ok("(nn/in 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0)");
|
||||
h.tick_sinks(1.0);
|
||||
REQUIRE(h.engine.sink_values[0][0] == Approx(1.0));
|
||||
REQUIRE(h.engine.sink_values[0][15] == Approx(0.0));
|
||||
}
|
||||
|
||||
SECTION("wrong arity is a compile error and binds nothing")
|
||||
{
|
||||
h.expect_error("(midi/cc74 0.1 0.2)", DiagnosticCategory::Arity);
|
||||
|
|
@ -338,7 +367,6 @@ TEST_CASE("registered cold commands dispatch to the handler", "[ext_registry]")
|
|||
REQUIRE(register_external_command(internSymbol("meml/cmd"), 1, 2));
|
||||
set_cold_command_handler(&test_command_handler, &cookie);
|
||||
const SymbolID cmd_id = internSymbol("meml/cmd");
|
||||
|
||||
SECTION("integer and float arguments parse by shape")
|
||||
{
|
||||
h.eval_ok("(meml/cmd 3)");
|
||||
|
|
@ -359,8 +387,8 @@ TEST_CASE("registered cold commands dispatch to the handler", "[ext_registry]")
|
|||
h.eval_ok("(meml/cmd level [1 2 3])");
|
||||
REQUIRE(g_capture.nargs == 2);
|
||||
REQUIRE(g_capture.args[0].kind == ColdArg::Kind::Symbol);
|
||||
REQUIRE(g_capture.args[0].symbol == internSymbol("level"));
|
||||
REQUIRE(g_capture.args[1].kind == ColdArg::Kind::Vector);
|
||||
REQUIRE(g_capture.args[1].vec_len == 3);
|
||||
REQUIRE(g_capture.args[1].vec[0] == Approx(1.0f));
|
||||
REQUIRE(g_capture.args[1].vec[1] == Approx(2.0f));
|
||||
REQUIRE(g_capture.args[1].vec[2] == Approx(3.0f));
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
//
|
||||
// Mode A (FailureMode::LkgFallback, DEFAULT): a non-finite value reaching an
|
||||
// output root substitutes the last-known-good value (or 0 with no LKG),
|
||||
// sets the pool's runtime_fallback_mask bit, and never zeroes per node.
|
||||
// sets the pool's runtime_fallback[] entry, and never zeroes per node.
|
||||
// Mode B (FailureMode::ZeroSquash, legacy): every non-finite node result is
|
||||
// clamped to 0.0; no fallback, no diagnostic.
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ struct Harness {
|
|||
}
|
||||
|
||||
bool in_fallback(int output_index) const {
|
||||
return (engine.pool.runtime_fallback_mask >> output_index) & 1;
|
||||
return engine.pool.runtime_fallback_bit(output_index);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ TEST_CASE("First failure has Error health until a finite root establishes LKG",
|
|||
// immediately while retaining its finite LKG as a safety net.
|
||||
h.eval_ok("(a1 7)");
|
||||
REQUIRE(output_health(h.engine.pool, 0) == OutputHealth::Running);
|
||||
REQUIRE((h.engine.pool.runtime_fallback_mask & 1u) == 0);
|
||||
REQUIRE_FALSE(h.engine.pool.runtime_fallback_bit(0));
|
||||
REQUIRE(h.engine.pool.outputs[0].has_lkg);
|
||||
}
|
||||
|
||||
|
|
@ -181,14 +181,14 @@ TEST_CASE("Unassign clears runtime and reactive health with the program",
|
|||
h.eval_ok("(define health-unassign-dep 1)");
|
||||
h.eval_ok("(a1 (* health-unassign-dep (* t 1e308)))");
|
||||
REQUIRE(h.tick(0, 2.0) == Approx(0.0));
|
||||
REQUIRE((h.engine.pool.runtime_fallback_mask & 1u) != 0);
|
||||
REQUIRE(h.engine.pool.runtime_fallback_bit(0));
|
||||
|
||||
h.eval_ok("(defn health-unassign-dep [x] x)");
|
||||
REQUIRE(h.engine.output_compile_diagnostics[0].active);
|
||||
|
||||
h.eval_ok("(unassign a1)");
|
||||
REQUIRE(output_health(h.engine.pool, 0) == OutputHealth::Idle);
|
||||
REQUIRE((h.engine.pool.runtime_fallback_mask & 1u) == 0);
|
||||
REQUIRE_FALSE(h.engine.pool.runtime_fallback_bit(0));
|
||||
REQUIRE_FALSE(h.engine.output_compile_diagnostics[0].active);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -308,7 +308,7 @@ TEST_CASE("Phase 4: non-finite value handling", "[phase4][numerical]") {
|
|||
double val = h.sample("a1", 0.0);
|
||||
REQUIRE(std::isfinite(val));
|
||||
REQUIRE(val == Approx(0.0));
|
||||
REQUIRE((h.engine.pool.runtime_fallback_mask & 1u) != 0);
|
||||
REQUIRE(h.engine.pool.runtime_fallback_bit(0));
|
||||
}
|
||||
|
||||
SECTION("legitimate large values stay finite") {
|
||||
|
|
@ -342,7 +342,7 @@ TEST_CASE("Phase 4: non-finite value handling", "[phase4][numerical]") {
|
|||
double val = h.sample("a1", 0.0);
|
||||
REQUIRE(std::isfinite(val));
|
||||
REQUIRE(val == Approx(0.0));
|
||||
REQUIRE((h.engine.pool.runtime_fallback_mask & 1u) != 0);
|
||||
REQUIRE(h.engine.pool.runtime_fallback_bit(0));
|
||||
}
|
||||
|
||||
SECTION("chained operations producing intermediate infinities stay finite") {
|
||||
|
|
@ -351,7 +351,7 @@ TEST_CASE("Phase 4: non-finite value handling", "[phase4][numerical]") {
|
|||
h.assign_ok("a1", "(* (/ 1 0) 5)");
|
||||
double val = h.sample("a1", 0.0);
|
||||
REQUIRE(std::isfinite(val));
|
||||
REQUIRE((h.engine.pool.runtime_fallback_mask & 1u) != 0);
|
||||
REQUIRE(h.engine.pool.runtime_fallback_bit(0));
|
||||
}
|
||||
|
||||
SECTION("NaN guard on executor output") {
|
||||
|
|
|
|||
|
|
@ -906,7 +906,7 @@ TEST_CASE("Arithmetic edge cases produce finite outputs", "[robustness][edge]")
|
|||
double v = h.sample("a1", 0.0);
|
||||
REQUIRE(std::isfinite(v));
|
||||
REQUIRE(v == Approx(0.0));
|
||||
REQUIRE((h.engine.pool.runtime_fallback_mask & 1u) != 0);
|
||||
REQUIRE(h.engine.pool.runtime_fallback_bit(0));
|
||||
}
|
||||
|
||||
SECTION("Modulo by zero activates bootstrap LKG")
|
||||
|
|
@ -916,7 +916,7 @@ TEST_CASE("Arithmetic edge cases produce finite outputs", "[robustness][edge]")
|
|||
double v = h.sample("a1", 0.0);
|
||||
REQUIRE(std::isfinite(v));
|
||||
REQUIRE(v == Approx(0.0));
|
||||
REQUIRE((h.engine.pool.runtime_fallback_mask & 1u) != 0);
|
||||
REQUIRE(h.engine.pool.runtime_fallback_bit(0));
|
||||
}
|
||||
|
||||
SECTION("sqrt of negative is finite")
|
||||
|
|
|
|||
Loading…
Reference in a new issue