ModuLisp/src/signal_engine/graph_builder.h

472 lines
21 KiB
C
Raw Normal View History

#ifndef SIGNAL_ENGINE_GRAPH_BUILDER_H
#define SIGNAL_ENGINE_GRAPH_BUILDER_H
#include "types.h"
#include "token.h"
#include "node_pool.h"
#include "cell_store.h"
#include "state_registry.h"
#include "diagnostics.h"
#include <cstring>
namespace sig {
// ── Cross-output live-edit ID tracking ──────────────────────────────────────
// Shared across all build_output_graph calls within one eval batch.
// Detects duplicate :id across separate output compilations.
struct SharedLiveEditIDs {
char ids[MAX_LIVE_SLOTS][MAX_LIVE_SLOT_ID] = {};
uint16_t count = 0;
void clear() { count = 0; }
bool contains(const char* id) const {
for (uint16_t i = 0; i < count; i++) {
if (strncmp(ids[i], id, MAX_LIVE_SLOT_ID) == 0) return true;
}
return false;
}
void add(const char* id) {
if (count < MAX_LIVE_SLOTS) {
const size_t length = strnlen(id, MAX_LIVE_SLOT_ID - 1);
memcpy(ids[count], id, length);
ids[count][length] = '\0';
count++;
}
}
};
// ── Scope (local bindings for let, lambda params, for variables) ────────────
struct Scope {
struct Binding {
SymbolID name;
uint16_t node_index;
};
Binding locals[MAX_LOCAL_BINDINGS] = {};
uint8_t local_count = 0;
Scope* parent = nullptr;
// Returns false if the local-binding pool is full (caller must report a
// diagnostic — a dropped binding would silently resolve to the wrong value).
bool bind(SymbolID name, uint16_t node_index);
const Binding* find(SymbolID name) const;
};
// ── Time Context ────────────────────────────────────────────────────────────
struct TimeContext {
uint16_t t_node; // node index representing "current t" (may be transformed)
};
// ── Bounded graph candidate IR ─────────────────────────────────────────────
// A successful build identifies a root and its complete retained dependency
// set, but is not published by GraphBuilder. Callers keep it behind a
// GraphMutationTransaction until source capacity and enclosing-form validation
// succeed, then install it through the cold evaluator's publication stage.
struct GraphBuildResult {
uint16_t root_node = NODE_NONE;
Diagnostic diagnostics[MAX_DIAGNOSTICS] = {};
uint8_t diagnostic_count = 0;
bool has_error = false;
// Cell dependencies discovered during graph compilation
CellIndex dep_cells[MAX_OUTPUT_DEPS] = {};
uint8_t dep_count = 0;
};
// ── Graph Builder ───────────────────────────────────────────────────────────
struct StateResourceRegistry;
enum class SymbolCategory : uint8_t {
none,
arith,
cmp,
logic,
unary,
binary,
ternary,
time_warp,
control,
signal,
side_effect,
waveform
};
enum class OperatorInputDomain : uint8_t { None, Angle, Phase, Scalar };
enum class OperatorOutputRange : uint8_t { None, Unipolar, Bipolar };
enum class OperatorRegime : uint8_t { None, Pure, PureShaper, Stateful, TopLevel };
enum class ColdEvaluable : uint8_t { No, Yes };
enum class BareIdentity : uint8_t {
Native,
NormalizedUnipolar,
NormalizedBipolar,
UnipolarShaper,
LfoUnipolar,
LfoBipolar
};
enum class OperatorNamespace : uint8_t {
None,
Normalized,
Radians,
Unipolar,
Bipolar,
Lfo,
BipolarLfo,
Osc,
Cold,
Raw,
Unknown,
Malformed
};
struct OperatorDeclaration {
SymbolID symbol = SymbolIntern::INVALID_ID;
const char* spelling = nullptr;
SymbolCategory category = SymbolCategory::none;
OperatorInputDomain input_domain = OperatorInputDomain::None;
OperatorOutputRange output_range = OperatorOutputRange::None;
OperatorRegime regime = OperatorRegime::None;
ColdEvaluable cold_evaluable = ColdEvaluable::No;
BareIdentity bare_identity = BareIdentity::Native;
};
struct NamespacedOperator {
OperatorNamespace name_space = OperatorNamespace::None;
SymbolID base_symbol = SymbolIntern::INVALID_ID;
const OperatorDeclaration* declaration = nullptr;
uint16_t namespace_span_len = 0;
};
struct GraphBuilder {
NodePool& pool;
CellStore& cells;
const SourceArena& source;
const char* source_base = nullptr; // raw tokenized text for string resolution
StateResourceRegistry* registry = nullptr;
// Diagnostics output
Diagnostic diagnostics[MAX_DIAGNOSTICS] = {};
uint8_t diagnostic_count = 0;
bool has_error = false;
// Dependency tracking (populated during build)
CellIndex dep_cells[MAX_OUTPUT_DEPS] = {};
uint8_t dep_count = 0;
// Recursion guard for inline stack
SymbolID inline_stack[MAX_INLINE_DEPTH] = {};
uint8_t inline_depth = 0;
// Syntactic nesting-depth guard for compile_expr recursion (RP2040 stack
// protection — see MAX_COMPILE_DEPTH in types.h).
uint16_t compile_depth = 0;
// Live-edit: slot count at build start (to detect fresh allocations vs pre-existing)
uint16_t live_slot_count_at_start = 0;
// Live-edit ids seen during this build (duplicate detection within one graph).
// Capped at 64 per single output — a single output won't have 256 live-edits.
static constexpr uint16_t MAX_IDS_PER_BUILD = 64;
char live_edit_ids_seen[MAX_IDS_PER_BUILD][MAX_LIVE_SLOT_ID] = {};
uint16_t live_edit_ids_count = 0;
// Cross-output shared ID tracking (set by build_output_graph when provided)
SharedLiveEditIDs* shared_live_edit_ids = nullptr;
struct LiveSlotUndo {
uint16_t slot_index = 0;
Sample value = 0.0;
Sample min_val = 0.0;
Sample max_val = 1.0;
Sample seed = 0.0;
};
static constexpr uint16_t MAX_LIVE_SLOT_UNDO =
MAX_LIVE_SLOTS < MAX_IDS_PER_BUILD ? MAX_LIVE_SLOTS : MAX_IDS_PER_BUILD;
LiveSlotUndo live_slot_undo[MAX_LIVE_SLOT_UNDO] = {};
uint16_t live_slot_undo_count = 0;
// Context flag: when true, compile_live_edit emits an error
bool reject_live_edit = false;
// Anonymous state identity (state-identity.md §2.5): which program this
// build compiles (output index, or MAX_OUTPUTS + state slot for defstate
// update graphs), and the ordinal of the next anonymous state allocation
// within this build. Together they form a structural/positional key so
// recompiles of the same program REUSE state slots via the registry
// instead of leaking a fresh slot per compile.
uint16_t anon_state_context = ANON_STATE_CONTEXT_NONE;
uint16_t anon_state_ordinal = 0;
// Duplicate-active-:id detection (state-identity.md §5.3/§8.1): slots
// claimed with an update root during THIS build. Two stateful forms in
// one compiled graph resolving to the same StateResourceKey would both
// install update roots on the same slot — ambiguous, must be rejected.
bool slot_claimed_this_build[MAX_STATE_SLOTS] = {};
// ── Well-known symbol IDs (populated at init) ───────────────────────
// Generated from symbols.def — do not edit by hand.
struct Symbols {
#define SYM(field, str, cat) SymbolID field;
#include "symbols.def"
#undef SYM
};
static Symbols sym;
static void init_symbols();
static bool symbols_initialized;
static constexpr uint16_t MAX_OPERATOR_DECLARATIONS = 192;
static OperatorDeclaration operator_declarations[MAX_OPERATOR_DECLARATIONS];
static uint16_t operator_declaration_count;
static const OperatorDeclaration* find_operator_declaration(SymbolID symbol);
static NamespacedOperator resolve_namespaced_operator(SymbolID symbol);
static bool namespace_applies(OperatorNamespace name_space,
const OperatorDeclaration& declaration);
// ── Form dispatch table ─────────────────────────────────────────────
struct FormEntry {
SymbolID sym;
uint16_t (GraphBuilder::*handler)(TokenStream&, Scope&, TimeContext&);
};
static constexpr uint16_t FORM_TABLE_CAPACITY = 64;
static FormEntry form_table[FORM_TABLE_CAPACITY];
static uint16_t form_table_count;
static bool form_table_sorted;
static void init_form_table();
// ── Construction ────────────────────────────────────────────────────
GraphBuilder(NodePool& pool, CellStore& cells, const SourceArena& source);
// ── Compilation entry points ────────────────────────────────────────
uint16_t compile_expr(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_inline_lambda_call(TokenStream& ts, Scope& scope,
TimeContext& ctx);
uint16_t compile_symbol(SymbolID sym, Scope& scope, TimeContext& ctx,
uint16_t span_start, uint16_t span_len);
uint16_t compile_form(SymbolID op, TokenStream& ts, Scope& scope,
TimeContext& ctx, Token op_tok);
uint16_t compile_namespaced_form(const NamespacedOperator& resolved,
TokenStream& ts, Scope& scope,
TimeContext& ctx, Token op_tok);
// ── Form handlers ───────────────────────────────────────────────────
// Time transforms
uint16_t compile_fast(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_slow(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_offset(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_loop_at(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_eval_at_time(TokenStream& ts, Scope& scope, TimeContext& ctx);
// Control flow
uint16_t compile_if(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_let(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_do(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_for(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_while_gate(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_lambda(TokenStream& ts, Scope& scope, TimeContext& ctx);
// Output feedback
uint16_t compile_prev(TokenStream& ts, Scope& scope, TimeContext& ctx);
// User-defined function call
uint16_t compile_call(SymbolID fn_sym, TokenStream& ts, Scope& scope,
TimeContext& ctx, Token op_tok);
// Arithmetic and math
uint16_t compile_variadic_arithmetic(SymbolID op, TokenStream& ts,
Scope& scope, TimeContext& ctx);
uint16_t compile_comparison(SymbolID op, TokenStream& ts,
Scope& scope, TimeContext& ctx);
uint16_t compile_logic(SymbolID op, TokenStream& ts,
Scope& scope, TimeContext& ctx);
uint16_t compile_unary_math(NodeOp op, TokenStream& ts,
Scope& scope, TimeContext& ctx);
uint16_t compile_binary_math(NodeOp op, TokenStream& ts,
Scope& scope, TimeContext& ctx);
uint16_t compile_binary_math_swapped(NodeOp op, TokenStream& ts,
Scope& scope, TimeContext& ctx);
uint16_t compile_ternary_math(NodeOp op, TokenStream& ts,
Scope& scope, TimeContext& ctx);
// Domain-specific signal functions
uint16_t compile_step(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_gates(SymbolID op, TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_euclid(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_seq(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_interp(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_dm(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_gatesw(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_range(TokenStream& ts, Scope& scope, TimeContext& ctx);
// Ratio-rhythm functions
uint16_t compile_rpulse(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_rstep(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_ridx(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_rwarp(TokenStream& ts, Scope& scope, TimeContext& ctx);
// State
uint16_t compile_integrate(TokenStream& ts, Scope& scope, TimeContext& ctx);
// UGens
uint16_t compile_phasor(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_lfo(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_blfo(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_lfo_sin(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_lfo_tri(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_lfo_saw(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_lfo_sqr(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_slew(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_one_pole(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_env_follow(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_sah(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_noise(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_toggle(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_count(TokenStream& ts, Scope& scope, TimeContext& ctx);
// UGen helpers
uint16_t alloc_state_slot(Sample init_value);
uint16_t report_unknown_ugen_keyword(const Token& kw);
uint16_t resolve_or_alloc(StateID state_id, ResourceKind kind, uint8_t role,
Sample init_value);
uint16_t build_lfo(TokenStream& ts, Scope& scope, TimeContext& ctx, uint16_t default_wave);
uint16_t build_osc_output(uint16_t state_load, uint16_t wave_type, uint16_t pw_node);
uint16_t compile_lfo_namespace(SymbolID waveform, bool bipolar,
TokenStream& ts, Scope& scope,
TimeContext& ctx, Token op_tok);
uint16_t compile_waveform(SymbolID waveform, OperatorNamespace name_space,
TokenStream& ts, Scope& scope,
TimeContext& ctx, Token op_tok);
uint16_t compile_cold_namespace(SymbolID op, TokenStream& ts,
Scope& scope, TimeContext& ctx,
Token op_tok);
// Live-edit
uint16_t compile_live_edit(TokenStream& ts, Scope& scope, TimeContext& ctx);
void remember_live_slot(uint16_t slot_index);
void rollback_live_slots();
// Random / hash
uint16_t compile_random(TokenStream& ts, Scope& scope, TimeContext& ctx);
uint16_t compile_index_rand(TokenStream& ts, Scope& scope, TimeContext& ctx);
// Vector literal [1 2 3]
uint16_t compile_vector_literal(TokenStream& ts, Scope& scope, TimeContext& ctx);
// ── Temporal templates ──────────────────────────────────────────────
uint16_t expand_beat(TimeContext& ctx);
uint16_t expand_bar(TimeContext& ctx);
uint16_t expand_phrase(TimeContext& ctx);
uint16_t expand_section(TimeContext& ctx);
uint16_t expand_beat_num(TimeContext& ctx);
uint16_t expand_bar_num(TimeContext& ctx);
// ── Collection resolution (for `for` loops) ─────────────────────────
struct Collection {
uint16_t element_nodes[64] = {};
uint16_t count = 0;
bool ok = false;
};
Collection resolve_collection(TokenStream& ts, Scope& scope, TimeContext& ctx);
Collection resolve_literal_collection(TokenStream& ts, Scope& scope,
TimeContext& ctx);
Collection resolve_symbol_collection(TokenStream& ts);
Collection resolve_range_collection(TokenStream& ts, Scope& scope,
TimeContext& ctx);
void fill_range_collection(Collection& collection, Sample start,
Sample end, Sample step);
// ── Data table resolution ───────────────────────────────────────────
struct DataRef {
uint16_t table_id;
uint16_t length;
bool ok;
};
DataRef resolve_data_table(TokenStream& ts, Scope& scope, TimeContext& ctx);
// ── Dependency tracking ─────────────────────────────────────────────
void add_dependency(SymbolID sym);
// ── Inline stack ────────────────────────────────────────────────────
bool is_in_inline_stack(SymbolID sym) const;
void push_inline_stack(SymbolID sym);
void pop_inline_stack();
// ── Inline expression cell ──────────────────────────────────────────
uint16_t inline_expression_cell(SymbolID sym, const CallableInfo& info,
Scope& scope, TimeContext& ctx);
// ── Error reporting ─────────────────────────────────────────────────
uint16_t report_error(const Token& tok, const char* message,
const char* suggestion = nullptr);
uint16_t report_error_at(uint16_t span_start, uint16_t span_len,
const char* message, const char* suggestion = nullptr);
// Category-aware error reporting — prefer these over the Runtime-defaulting overloads
uint16_t report_error_cat(DiagnosticCategory cat, const Token& tok,
const char* message, const char* suggestion = nullptr);
uint16_t report_error_at_cat(DiagnosticCategory cat,
uint16_t span_start, uint16_t span_len,
const char* message, const char* suggestion = nullptr);
uint16_t report_error_with_fuzzy_match(SymbolID sym,
uint16_t span_start, uint16_t span_len);
uint16_t report_warning(uint16_t span_start, uint16_t span_len,
const char* message, const char* suggestion = nullptr);
// ── Helpers ─────────────────────────────────────────────────────────
bool is_const(uint16_t node_idx) const;
Sample const_value(uint16_t node_idx) const;
bool is_side_effect_form(SymbolID op) const;
bool is_arithmetic_op(SymbolID op) const;
bool is_comparison_op(SymbolID op) const;
bool is_logic_op(SymbolID op) const;
bool is_unary_math(SymbolID op) const;
bool is_binary_math(SymbolID op) const;
bool is_ternary_math(SymbolID op) const;
static bool is_output_symbol(SymbolID op);
static uint16_t resolve_output_index(SymbolID op);
static uint16_t resolve_hardware_input(SymbolID sym);
NodeOp arithmetic_sym_to_op(SymbolID op) const;
NodeOp comparison_sym_to_op(SymbolID op) const;
NodeOp unary_sym_to_op(SymbolID op) const;
// Skip past a complete form in the token stream (for deferred parsing).
static void skip_form(TokenStream& ts);
void report_oversized_vector(TokenStream& ts, const Token& excess);
};
// Shared "Too many state variables (max N)" message with the build's actual
// MAX_STATE_SLOTS cap baked in (16 on firmware, 32 on desktop/WASM).
const char* state_slots_exhausted_msg();
// ── Top-level build function ────────────────────────────────────────────────
GraphBuildResult build_output_graph(
NodePool& pool,
TokenStream& ts,
CellStore& cells,
const SourceArena& source,
const char* source_base = nullptr,
StateResourceRegistry* registry = nullptr,
SharedLiveEditIDs* shared_ids = nullptr,
uint16_t anon_state_context = ANON_STATE_CONTEXT_NONE
);
} // namespace sig
#endif // SIGNAL_ENGINE_GRAPH_BUILDER_H