#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 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