@audiofab-io/fv1-core 0.2.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/blocks/ATL_DEVELOPER_REFERENCE.md +156 -0
  2. package/blocks/control/constant.atl +36 -0
  3. package/blocks/control/entropy_lfo.atl +74 -0
  4. package/blocks/control/envelope.atl +121 -0
  5. package/blocks/control/invert.atl +33 -0
  6. package/blocks/control/pot.atl +150 -0
  7. package/blocks/control/power.atl +77 -0
  8. package/blocks/control/ramp_lfo.atl +122 -0
  9. package/blocks/control/scale_offset.atl +84 -0
  10. package/blocks/control/sincos_lfo.atl +126 -0
  11. package/blocks/control/smoother.atl +48 -0
  12. package/blocks/control/tremolizer.atl +55 -0
  13. package/blocks/effects/delay/micro_stutter.atl +77 -0
  14. package/blocks/effects/delay/mn3011.atl +281 -0
  15. package/blocks/effects/delay/simple_delay.atl +96 -0
  16. package/blocks/effects/delay/triple_tap_delay.atl +176 -0
  17. package/blocks/effects/lo-fi/bit_mangler.atl +74 -0
  18. package/blocks/effects/lo-fi/chiptune.atl +311 -0
  19. package/blocks/effects/lo-fi/tape_degrade.atl +181 -0
  20. package/blocks/effects/modulation/chorus.atl +141 -0
  21. package/blocks/effects/modulation/chorus_4voice.atl +188 -0
  22. package/blocks/effects/modulation/flanger.atl +184 -0
  23. package/blocks/effects/modulation/guitar_synth.atl +350 -0
  24. package/blocks/effects/modulation/harmonic_trem.atl +129 -0
  25. package/blocks/effects/modulation/organ_synth.atl +326 -0
  26. package/blocks/effects/modulation/phaser.atl +300 -0
  27. package/blocks/effects/pitch/octave_up_down.atl +80 -0
  28. package/blocks/effects/pitch/pitch_offset.atl +149 -0
  29. package/blocks/effects/pitch/pitch_offset_dual.atl +197 -0
  30. package/blocks/effects/pitch/pitch_shift.atl +115 -0
  31. package/blocks/effects/pitch/sub_octave.atl +100 -0
  32. package/blocks/effects/reverb/ducking_reverb.atl +145 -0
  33. package/blocks/effects/reverb/min_reverb.atl +132 -0
  34. package/blocks/effects/reverb/plate_reverb.atl +344 -0
  35. package/blocks/effects/reverb/room_reverb.atl +293 -0
  36. package/blocks/effects/reverb/smear.atl +90 -0
  37. package/blocks/effects/reverb/spring_reverb.atl +353 -0
  38. package/blocks/filter/1p_high_pass.atl +63 -0
  39. package/blocks/filter/1p_low_pass.atl +59 -0
  40. package/blocks/filter/auto_wah.atl +207 -0
  41. package/blocks/filter/bbd_loss.atl +79 -0
  42. package/blocks/filter/shelving_high_pass.atl +76 -0
  43. package/blocks/filter/shelving_low_pass.atl +76 -0
  44. package/blocks/filter/svf_2p.atl +116 -0
  45. package/blocks/gain_mix/crossfade.atl +93 -0
  46. package/blocks/gain_mix/crossfade2.atl +86 -0
  47. package/blocks/gain_mix/crossfade3.atl +71 -0
  48. package/blocks/gain_mix/gainboost.atl +54 -0
  49. package/blocks/gain_mix/mixer2.atl +76 -0
  50. package/blocks/gain_mix/mixer3.atl +109 -0
  51. package/blocks/gain_mix/mixer4.atl +152 -0
  52. package/blocks/gain_mix/volume.atl +51 -0
  53. package/blocks/io/adc.atl +53 -0
  54. package/blocks/io/dac.atl +61 -0
  55. package/blocks/other/stickynote.atl +24 -0
  56. package/blocks/other/tone_gen_adjustable.atl +137 -0
  57. package/blocks/other/tone_gen_fixed.atl +109 -0
  58. package/dist/blockDiagram/blocks/BlockDirectoryLoader.d.ts +13 -0
  59. package/dist/blockDiagram/blocks/BlockDirectoryLoader.d.ts.map +1 -0
  60. package/dist/blockDiagram/blocks/BlockDirectoryLoader.js +44 -0
  61. package/dist/blockDiagram/blocks/BlockDirectoryLoader.js.map +1 -0
  62. package/dist/blockDiagram/blocks/BlockRegistry.d.ts +48 -0
  63. package/dist/blockDiagram/blocks/BlockRegistry.d.ts.map +1 -0
  64. package/dist/blockDiagram/blocks/BlockRegistry.js +109 -0
  65. package/dist/blockDiagram/blocks/BlockRegistry.js.map +1 -0
  66. package/dist/blockDiagram/blocks/TemplateBlock.d.ts +20 -0
  67. package/dist/blockDiagram/blocks/TemplateBlock.d.ts.map +1 -0
  68. package/dist/blockDiagram/blocks/TemplateBlock.js +82 -0
  69. package/dist/blockDiagram/blocks/TemplateBlock.js.map +1 -0
  70. package/dist/blockDiagram/blocks/base/BaseBlock.d.ts +248 -0
  71. package/dist/blockDiagram/blocks/base/BaseBlock.d.ts.map +1 -0
  72. package/dist/blockDiagram/blocks/base/BaseBlock.js +402 -0
  73. package/dist/blockDiagram/blocks/base/BaseBlock.js.map +1 -0
  74. package/dist/blockDiagram/builtinBlocks.d.ts +9 -0
  75. package/dist/blockDiagram/builtinBlocks.d.ts.map +1 -0
  76. package/dist/blockDiagram/builtinBlocks.js +4912 -0
  77. package/dist/blockDiagram/builtinBlocks.js.map +1 -0
  78. package/dist/blockDiagram/compiler/BlockTemplate.d.ts +37 -0
  79. package/dist/blockDiagram/compiler/BlockTemplate.d.ts.map +1 -0
  80. package/dist/blockDiagram/compiler/BlockTemplate.js +860 -0
  81. package/dist/blockDiagram/compiler/BlockTemplate.js.map +1 -0
  82. package/dist/blockDiagram/compiler/CodeOptimizer.d.ts +75 -0
  83. package/dist/blockDiagram/compiler/CodeOptimizer.d.ts.map +1 -0
  84. package/dist/blockDiagram/compiler/CodeOptimizer.js +443 -0
  85. package/dist/blockDiagram/compiler/CodeOptimizer.js.map +1 -0
  86. package/dist/blockDiagram/compiler/GraphCompiler.d.ts +63 -0
  87. package/dist/blockDiagram/compiler/GraphCompiler.d.ts.map +1 -0
  88. package/dist/blockDiagram/compiler/GraphCompiler.js +656 -0
  89. package/dist/blockDiagram/compiler/GraphCompiler.js.map +1 -0
  90. package/dist/blockDiagram/compiler/TopologicalSort.d.ts +63 -0
  91. package/dist/blockDiagram/compiler/TopologicalSort.d.ts.map +1 -0
  92. package/dist/blockDiagram/compiler/TopologicalSort.js +268 -0
  93. package/dist/blockDiagram/compiler/TopologicalSort.js.map +1 -0
  94. package/dist/blockDiagram/index.d.ts +30 -0
  95. package/dist/blockDiagram/index.d.ts.map +1 -0
  96. package/dist/blockDiagram/index.js +29 -0
  97. package/dist/blockDiagram/index.js.map +1 -0
  98. package/dist/blockDiagram/types/Block.d.ts +178 -0
  99. package/dist/blockDiagram/types/Block.d.ts.map +1 -0
  100. package/dist/blockDiagram/types/Block.js +5 -0
  101. package/dist/blockDiagram/types/Block.js.map +1 -0
  102. package/dist/blockDiagram/types/CodeGenContext.d.ts +235 -0
  103. package/dist/blockDiagram/types/CodeGenContext.d.ts.map +1 -0
  104. package/dist/blockDiagram/types/CodeGenContext.js +554 -0
  105. package/dist/blockDiagram/types/CodeGenContext.js.map +1 -0
  106. package/dist/blockDiagram/types/Connection.d.ts +17 -0
  107. package/dist/blockDiagram/types/Connection.d.ts.map +1 -0
  108. package/dist/blockDiagram/types/Connection.js +5 -0
  109. package/dist/blockDiagram/types/Connection.js.map +1 -0
  110. package/dist/blockDiagram/types/Graph.d.ts +28 -0
  111. package/dist/blockDiagram/types/Graph.d.ts.map +1 -0
  112. package/dist/blockDiagram/types/Graph.js +24 -0
  113. package/dist/blockDiagram/types/Graph.js.map +1 -0
  114. package/dist/blockDiagram/types/IR.d.ts +79 -0
  115. package/dist/blockDiagram/types/IR.d.ts.map +1 -0
  116. package/dist/blockDiagram/types/IR.js +6 -0
  117. package/dist/blockDiagram/types/IR.js.map +1 -0
  118. package/dist/blockDiagram/utils/SpinCADConverter.d.ts +17 -0
  119. package/dist/blockDiagram/utils/SpinCADConverter.d.ts.map +1 -0
  120. package/dist/blockDiagram/utils/SpinCADConverter.js +307 -0
  121. package/dist/blockDiagram/utils/SpinCADConverter.js.map +1 -0
  122. package/dist/effect/compileEffect.d.ts +51 -0
  123. package/dist/effect/compileEffect.d.ts.map +1 -0
  124. package/dist/effect/compileEffect.js +133 -0
  125. package/dist/effect/compileEffect.js.map +1 -0
  126. package/dist/index.d.ts +2 -0
  127. package/dist/index.d.ts.map +1 -1
  128. package/dist/index.js +2 -0
  129. package/dist/index.js.map +1 -1
  130. package/package.json +17 -5
@@ -0,0 +1,656 @@
1
+ /**
2
+ * Main graph compiler
3
+ * Orchestrates the compilation of a block diagram to FV-1 assembly
4
+ */
5
+ import { TopologicalSort } from './TopologicalSort.js';
6
+ import { CodeGenerationContext } from '../types/CodeGenContext.js';
7
+ import { CodeOptimizer, OptimizationLevel } from './CodeOptimizer.js';
8
+ import { FV1Assembler } from '../../assembler/FV1Assembler.js';
9
+ export class GraphCompiler {
10
+ constructor(registry) {
11
+ this.registry = registry;
12
+ this.topologicalSort = new TopologicalSort();
13
+ this.optimizer = new CodeOptimizer();
14
+ }
15
+ /**
16
+ * Compile a block diagram to FV-1 assembly code
17
+ */
18
+ compile(graph, options) {
19
+ const errors = [];
20
+ const warnings = [];
21
+ // 1. Validate graph structure
22
+ const validation = this.validateGraph(graph);
23
+ if (!validation.valid) {
24
+ return {
25
+ success: false,
26
+ errors: validation.errors,
27
+ statistics: {
28
+ instructionsUsed: 0,
29
+ registersUsed: 0,
30
+ memoryUsed: 0,
31
+ blocksProcessed: 0,
32
+ lfosUsed: 0,
33
+ usedLFOs: []
34
+ }
35
+ };
36
+ }
37
+ if (validation.warnings) {
38
+ warnings.push(...validation.warnings);
39
+ }
40
+ // 2. Topological sort to determine execution order
41
+ const sortResult = this.topologicalSort.sort(graph);
42
+ if (!sortResult.success) {
43
+ return {
44
+ success: false,
45
+ errors: [sortResult.error || 'Failed to sort blocks'],
46
+ statistics: {
47
+ instructionsUsed: 0,
48
+ registersUsed: 0,
49
+ memoryUsed: 0,
50
+ blocksProcessed: 0,
51
+ lfosUsed: 0,
52
+ usedLFOs: []
53
+ }
54
+ };
55
+ }
56
+ const executionOrder = sortResult.order;
57
+ // 3. Create code generation context
58
+ const context = new CodeGenerationContext(graph, { delaySize: options.delaySize });
59
+ // 4. Pre-allocate registers for all connected outputs
60
+ // This is necessary for feedback loops where blocks may read from outputs
61
+ // that haven't been generated yet in the execution order
62
+ this.preallocateAllOutputs(graph, context);
63
+ // 5. Enable section flattening if Aggressive optimization is selected
64
+ const level = options.optimizationLevel !== undefined ? options.optimizationLevel : OptimizationLevel.Aggressive;
65
+ if (level >= OptimizationLevel.Aggressive) {
66
+ context.setFlattenMode(true);
67
+ }
68
+ // 5. Generate code for each block in execution order
69
+ // Blocks will push their code to appropriate sections or push IR nodes
70
+ try {
71
+ // First, process sticky notes (header comments)
72
+ for (const block of graph.blocks) {
73
+ if (!executionOrder.includes(block.id) && block.type.includes('stickynote')) {
74
+ const definition = this.registry.getBlock(block.type);
75
+ if (definition) {
76
+ context.setCurrentBlock(block.id);
77
+ definition.generateCode(context);
78
+ }
79
+ }
80
+ }
81
+ // Then process connected blocks in execution order
82
+ for (const blockId of executionOrder) {
83
+ const block = graph.blocks.find(b => b.id === blockId);
84
+ if (!block)
85
+ continue;
86
+ const definition = this.registry.getBlock(block.type);
87
+ if (!definition) {
88
+ errors.push(`Unknown block type: ${block.type}`);
89
+ continue;
90
+ }
91
+ context.setCurrentBlock(blockId);
92
+ // Add block comment ONLY in flattened mode (Level 2)
93
+ if (context.isFlattenMode()) {
94
+ const blockComments = this.generateBlockComment(block, context, graph);
95
+ context.pushMainCode(...blockComments);
96
+ }
97
+ // If it's a template-based block (to be implemented), it will push to IR
98
+ // For now, even legacy blocks can be adapted to push IR if they want
99
+ definition.generateCode(context);
100
+ const blockErrors = context.getErrors();
101
+ if (blockErrors.length > 0) {
102
+ errors.push(...blockErrors);
103
+ }
104
+ context.resetScratchRegisters();
105
+ }
106
+ // Short-circuit completely if template generation produced fatal errors
107
+ if (errors.length > 0) {
108
+ return {
109
+ success: false,
110
+ errors: errors,
111
+ statistics: {
112
+ instructionsUsed: 0,
113
+ registersUsed: 0,
114
+ memoryUsed: 0,
115
+ blocksProcessed: 0,
116
+ lfosUsed: 0,
117
+ usedLFOs: []
118
+ }
119
+ };
120
+ }
121
+ }
122
+ catch (error) {
123
+ return {
124
+ success: false,
125
+ errors: [`Code generation failed: ${error}`]
126
+ };
127
+ }
128
+ // 6. Process IR and optimize (Move Pruning)
129
+ const irResult = this.processIR(context);
130
+ const sections = irResult.sections;
131
+ warnings.push(...irResult.warnings);
132
+ // 7. Assemble the final program with proper structure
133
+ const codeLines = [];
134
+ // Section 1: Header comment
135
+ codeLines.push(';================================================================================');
136
+ codeLines.push(`; ${graph.metadata?.name || 'Untitled Diagram'}`);
137
+ if (graph.metadata?.description) {
138
+ codeLines.push(`; ${graph.metadata.description}`);
139
+ }
140
+ if (graph.metadata?.author) {
141
+ codeLines.push(`; Author: ${graph.metadata.author}`);
142
+ }
143
+ codeLines.push(`; Generated at ${new Date().toLocaleString()} by the Audiofab Easy Spin (FV-1)`);
144
+ codeLines.push('; Block Diagram Editor (https://www.audiofab.com/)');
145
+ codeLines.push(';================================================================================');
146
+ // Add any header comments from sticky notes
147
+ const headerComments = context.getHeaderComments();
148
+ if (headerComments.length > 0) {
149
+ codeLines.push('');
150
+ codeLines.push(...headerComments);
151
+ }
152
+ // Add pot mapping comments
153
+ const potMappings = this.generatePotMappingComments(graph);
154
+ if (potMappings.length > 0) {
155
+ codeLines.push('');
156
+ codeLines.push(...potMappings);
157
+ }
158
+ codeLines.push('');
159
+ // Section 1.5: IR Header (EQU, MEM from blocks)
160
+ if (irResult.sections.header && irResult.sections.header.length > 0) {
161
+ codeLines.push('; Block Declarations');
162
+ codeLines.push(';--------------------------------------------------------------------------------');
163
+ codeLines.push(...irResult.sections.header);
164
+ codeLines.push('');
165
+ }
166
+ // Section 2: Initialization (EQU, MEM, SKP)
167
+ if (sections.init.length > 0) {
168
+ codeLines.push('; Initialization');
169
+ codeLines.push(';--------------------------------------------------------------------------------');
170
+ codeLines.push(...sections.init);
171
+ codeLines.push('');
172
+ }
173
+ // Section 3: Input Section (ADC reads, POT reads)
174
+ if (!context.isFlattenMode() && sections.input.length > 0) {
175
+ codeLines.push('; Input Section');
176
+ codeLines.push(';--------------------------------------------------------------------------------');
177
+ codeLines.push(...sections.input);
178
+ codeLines.push('');
179
+ }
180
+ // Section 4: Main Program (or Flattened Execution)
181
+ if (sections.main.length > 0) {
182
+ if (context.isFlattenMode()) {
183
+ codeLines.push('; Flattened Execution (TOPOLOGICAL ORDER)');
184
+ }
185
+ else {
186
+ codeLines.push('; Main Program');
187
+ }
188
+ codeLines.push(';--------------------------------------------------------------------------------');
189
+ codeLines.push(...sections.main);
190
+ codeLines.push('');
191
+ }
192
+ // Section 5: Output Section (DAC writes)
193
+ if (!context.isFlattenMode() && sections.output.length > 0) {
194
+ codeLines.push('; Output Section');
195
+ codeLines.push(';--------------------------------------------------------------------------------');
196
+ codeLines.push(...sections.output);
197
+ codeLines.push('');
198
+ }
199
+ // Apply post-processing optimizations to the complete code
200
+ const optimizerResult = this.optimizer.optimize(codeLines, level);
201
+ // Post-optimization register limit check
202
+ // This runs AFTER dead store elimination and register pruning/renumbering,
203
+ // so it reflects the true minimum register requirement
204
+ const maxRegisters = options.regCount ?? 32;
205
+ if (optimizerResult.finalRegisterCount > maxRegisters) {
206
+ errors.push(`Program requires ${optimizerResult.finalRegisterCount} registers after optimization, ` +
207
+ `but the configured maximum is ${maxRegisters} (REG0-REG${maxRegisters - 1}). Remove some blocks to reduce register pressure.`);
208
+ }
209
+ // 7. Assemble the code to get accurate instruction count
210
+ let instructions = 0;
211
+ let lfosUsed = 0;
212
+ let usedLFOs = [];
213
+ let registersUsed = optimizerResult.finalRegisterCount;
214
+ try {
215
+ const assembler = new FV1Assembler({
216
+ fv1AsmMemBug: options.fv1AsmMemBug ?? true,
217
+ clampReals: options.clampReals ?? true,
218
+ regCount: options.regCount,
219
+ progSize: options.progSize,
220
+ delaySize: options.delaySize
221
+ });
222
+ const assemblyResult = assembler.assemble(optimizerResult.code.join('\n'));
223
+ // Count actual instructions from machine code (exclude NOP padding)
224
+ const NOP_ENCODING = 0x00000011;
225
+ instructions = assemblyResult.machineCode.filter((code) => code !== NOP_ENCODING).length;
226
+ // Use accurate register constraints and LFO metrics from the assembler
227
+ registersUsed = assemblyResult.usedRegistersCount;
228
+ lfosUsed = assemblyResult.usedLFOs.length;
229
+ usedLFOs = assemblyResult.usedLFOs;
230
+ // Check for assembly errors
231
+ const assemblyErrors = assemblyResult.problems.filter((p) => p.isfatal);
232
+ if (assemblyErrors.length > 0) {
233
+ assemblyErrors.forEach((p) => {
234
+ errors.push(p.message);
235
+ });
236
+ }
237
+ }
238
+ catch (e) {
239
+ // Fallback to rough estimate if assembly fails
240
+ instructions = optimizerResult.code.filter(line => {
241
+ const trimmed = line.trim();
242
+ return trimmed.length > 0 &&
243
+ !trimmed.startsWith(';') &&
244
+ !trimmed.includes('equ') &&
245
+ !trimmed.includes('mem') &&
246
+ !trimmed.includes(':'); // Skip labels
247
+ }).length;
248
+ warnings.push('Could not accurately count instructions');
249
+ }
250
+ // Check instruction limit
251
+ const maxProgSize = options.progSize;
252
+ if (instructions > maxProgSize) {
253
+ errors.push(`Program uses ${instructions} instructions, but FV-1 maximum is ${maxProgSize}. ` +
254
+ 'Reduce complexity or optimize blocks.');
255
+ }
256
+ else if (instructions > maxProgSize * 0.95) {
257
+ warnings.push(`Program uses ${instructions}/${maxProgSize} instructions. ` +
258
+ 'Very close to limit!');
259
+ }
260
+ // 8. Build statistics
261
+ const statistics = {
262
+ instructionsUsed: instructions,
263
+ registersUsed: registersUsed,
264
+ memoryUsed: context.getUsedMemorySize(),
265
+ blocksProcessed: executionOrder.length,
266
+ lfosUsed: lfosUsed,
267
+ usedLFOs: usedLFOs
268
+ };
269
+ // Add optimization info to warnings
270
+ if (optimizerResult.optimizationsApplied > 0) {
271
+ warnings.push(`Applied ${optimizerResult.optimizationsApplied} code optimization(s)`);
272
+ optimizerResult.details.forEach(detail => {
273
+ warnings.push(` - ${detail}`);
274
+ });
275
+ }
276
+ // Return result
277
+ if (errors.length > 0) {
278
+ return {
279
+ success: false,
280
+ assembly: optimizerResult.code.join('\n'), // Include assembly even with errors so it can be viewed
281
+ statistics, // Include statistics even on failure so status bar shows usage
282
+ errors,
283
+ warnings: warnings.length > 0 ? warnings : undefined
284
+ };
285
+ }
286
+ return {
287
+ success: true,
288
+ assembly: optimizerResult.code.join('\n'),
289
+ statistics,
290
+ warnings: warnings.length > 0 ? warnings : undefined
291
+ };
292
+ }
293
+ /**
294
+ * Generate a descriptive comment block for a block's code section
295
+ */
296
+ generateBlockComment(block, context, graph) {
297
+ const definition = this.registry.getBlock(block.type);
298
+ if (!definition) {
299
+ return [];
300
+ }
301
+ const lines = [];
302
+ lines.push(';===============================================================================');
303
+ lines.push(`; ${definition.name} (${block.id})`);
304
+ // Show inputs if any
305
+ if (definition.inputs.length > 0) {
306
+ const inputInfo = [];
307
+ for (const input of definition.inputs) {
308
+ const inputReg = context.getInputRegister(block.id, input.id);
309
+ if (inputReg) {
310
+ inputInfo.push(`${input.name}: ${inputReg}`);
311
+ }
312
+ else if (!input.required) {
313
+ inputInfo.push(`${input.name}: (not connected)`);
314
+ }
315
+ }
316
+ if (inputInfo.length > 0) {
317
+ lines.push(`; Inputs: ${inputInfo.join(', ')}`);
318
+ }
319
+ }
320
+ // Show outputs if any
321
+ if (definition.outputs.length > 0) {
322
+ const outputInfo = [];
323
+ for (const output of definition.outputs) {
324
+ // Get the register allocation for this output
325
+ const alloc = context.registerAllocations.find((a) => a.blockId === block.id && a.portId === output.id);
326
+ if (alloc) {
327
+ outputInfo.push(`${output.name}: ${alloc.alias}`);
328
+ }
329
+ }
330
+ if (outputInfo.length > 0) {
331
+ lines.push(`; Outputs: ${outputInfo.join(', ')}`);
332
+ }
333
+ }
334
+ // Show parameter values if any
335
+ if (definition.parameters.length > 0) {
336
+ const paramInfo = [];
337
+ for (const param of definition.parameters) {
338
+ const value = block.parameters[param.id];
339
+ if (value !== undefined) {
340
+ paramInfo.push(`${param.name}=${value}`);
341
+ }
342
+ }
343
+ if (paramInfo.length > 0) {
344
+ lines.push(`; Parameters: ${paramInfo.join(', ')}`);
345
+ }
346
+ }
347
+ lines.push(';-------------------------------------------------------------------------------');
348
+ return lines;
349
+ }
350
+ /**
351
+ * Validate the graph structure
352
+ */
353
+ validateGraph(graph) {
354
+ const errors = [];
355
+ const warnings = [];
356
+ // Check for blocks
357
+ if (graph.blocks.length === 0) {
358
+ // Empty graph is valid - just won't generate any code
359
+ warnings.push('Graph is empty - add some blocks to generate code');
360
+ return { valid: true, warnings };
361
+ }
362
+ // Check for at least one output block (warning only)
363
+ const hasOutput = graph.blocks.some(b => b.type.startsWith('output.'));
364
+ if (!hasOutput) {
365
+ warnings.push('Graph has no output blocks - add DACL or DACR to hear sound');
366
+ }
367
+ // Check for at least one input block (warning only)
368
+ const hasInput = graph.blocks.some(b => b.type.startsWith('input.'));
369
+ if (!hasInput) {
370
+ warnings.push('Graph has no input blocks - output will be silent');
371
+ }
372
+ // Validate each block's connections
373
+ for (const block of graph.blocks) {
374
+ const definition = this.registry.getBlock(block.type);
375
+ if (!definition) {
376
+ errors.push(`Unknown block type: ${block.type} (block ${block.id})`);
377
+ continue;
378
+ }
379
+ // Check required inputs are connected (warning only for better UX)
380
+ for (const input of definition.inputs) {
381
+ if (input.required) {
382
+ const hasConnection = graph.connections.some(c => c.to.blockId === block.id && c.to.portId === input.id);
383
+ if (!hasConnection) {
384
+ warnings.push(`Block '${definition.name}' (${block.id}) ` +
385
+ `has unconnected required input '${input.name}'`);
386
+ }
387
+ }
388
+ }
389
+ }
390
+ // Validate connections
391
+ for (const connection of graph.connections) {
392
+ // Check source block exists
393
+ const sourceBlock = graph.blocks.find(b => b.id === connection.from.blockId);
394
+ if (!sourceBlock) {
395
+ errors.push(`Connection references non-existent source block: ${connection.from.blockId}`);
396
+ continue;
397
+ }
398
+ // Check dest block exists
399
+ const destBlock = graph.blocks.find(b => b.id === connection.to.blockId);
400
+ if (!destBlock) {
401
+ errors.push(`Connection references non-existent destination block: ${connection.to.blockId}`);
402
+ continue;
403
+ }
404
+ // Check for self-loops
405
+ // (Removed strict validation to allow feedback connections just like SpinCAD)
406
+ // if (connection.from.blockId === connection.to.blockId) {
407
+ // errors.push(
408
+ // `Self-loop detected: Block ${sourceBlock.type} (${connection.from.blockId}) ` +
409
+ // `cannot have its output connected to its own input`
410
+ // );
411
+ // continue;
412
+ // }
413
+ // Check ports exist
414
+ const sourceDef = this.registry.getBlock(sourceBlock.type);
415
+ const destDef = this.registry.getBlock(destBlock.type);
416
+ if (!sourceDef || !destDef)
417
+ continue;
418
+ const sourcePort = sourceDef.outputs.find(p => p.id === connection.from.portId);
419
+ const destPort = destDef.inputs.find(p => p.id === connection.to.portId);
420
+ if (!sourcePort) {
421
+ errors.push(`Connection references non-existent output port '${connection.from.portId}' ` +
422
+ `on block ${connection.from.blockId}`);
423
+ continue;
424
+ }
425
+ if (!destPort) {
426
+ errors.push(`Connection references non-existent input port '${connection.to.portId}' ` +
427
+ `on block ${connection.to.blockId}`);
428
+ continue;
429
+ }
430
+ // Validate port type compatibility
431
+ if (sourcePort.type !== destPort.type) {
432
+ errors.push(`Port type mismatch: Cannot connect ${sourcePort.type} output ` +
433
+ `'${sourcePort.name}' from ${sourceDef.name} (${sourceBlock.id}) ` +
434
+ `to ${destPort.type} input '${destPort.name}' on ${destDef.name} (${destBlock.id}). ` +
435
+ `Port types must match (audio→audio or control→control).`);
436
+ }
437
+ }
438
+ // Check for multiple connections to the same input (multiple drivers)
439
+ const inputConnections = new Map();
440
+ for (const connection of graph.connections) {
441
+ const inputKey = `${connection.to.blockId}:${connection.to.portId}`;
442
+ if (!inputConnections.has(inputKey)) {
443
+ inputConnections.set(inputKey, []);
444
+ }
445
+ inputConnections.get(inputKey).push(connection.from.blockId);
446
+ }
447
+ for (const [inputKey, sources] of inputConnections.entries()) {
448
+ if (sources.length > 1) {
449
+ const [blockId, portId] = inputKey.split(':');
450
+ const block = graph.blocks.find(b => b.id === blockId);
451
+ const def = block ? this.registry.getBlock(block.type) : undefined;
452
+ const port = def?.inputs.find(p => p.id === portId);
453
+ errors.push(`Multiple connections to the same input: ` +
454
+ `${def?.name || 'Unknown'} (${blockId}) input '${port?.name || portId}' ` +
455
+ `has ${sources.length} connections. Each input can only have one source.`);
456
+ }
457
+ }
458
+ return {
459
+ valid: errors.length === 0,
460
+ errors: errors.length > 0 ? errors : undefined,
461
+ warnings: warnings.length > 0 ? warnings : undefined
462
+ };
463
+ }
464
+ /**
465
+ * Generate pot mapping comments showing which parameters each pot controls
466
+ */
467
+ generatePotMappingComments(graph) {
468
+ const comments = [];
469
+ const potMappings = new Map(); // pot number -> list of controlled parameters
470
+ // Find all pot blocks
471
+ for (const block of graph.blocks) {
472
+ if (block.type === 'input.pot') {
473
+ const potNumber = block.parameters['potNumber'] ?? 0;
474
+ // Find what this pot is connected to
475
+ const connections = graph.connections.filter(c => c.from.blockId === block.id);
476
+ if (connections.length > 0) {
477
+ const targets = [];
478
+ for (const conn of connections) {
479
+ const targetBlock = graph.blocks.find(b => b.id === conn.to.blockId);
480
+ if (targetBlock) {
481
+ const targetDef = this.registry.getBlock(targetBlock.type);
482
+ if (targetDef) {
483
+ const inputPort = targetDef.inputs.find(p => p.id === conn.to.portId);
484
+ const targetLabel = `${targetDef.name}${inputPort ? ` (${inputPort.name})` : ''}`;
485
+ targets.push(targetLabel);
486
+ }
487
+ }
488
+ }
489
+ if (targets.length > 0) {
490
+ if (!potMappings.has(potNumber)) {
491
+ potMappings.set(potNumber, []);
492
+ }
493
+ potMappings.get(potNumber).push(...targets);
494
+ }
495
+ }
496
+ }
497
+ }
498
+ // Generate comments for pots that are mapped
499
+ if (potMappings.size > 0) {
500
+ comments.push('; Potentiometer Assignments');
501
+ comments.push(';--------------------------------------------------------------------------------');
502
+ // Sort by pot number
503
+ const sortedPots = Array.from(potMappings.keys()).sort((a, b) => a - b);
504
+ for (const potNumber of sortedPots) {
505
+ const targets = potMappings.get(potNumber);
506
+ if (targets.length > 0) {
507
+ comments.push(`; POT${potNumber}: ${targets.join(', ')}`);
508
+ }
509
+ }
510
+ }
511
+ return comments;
512
+ }
513
+ /**
514
+ * Pre-allocate registers for all connected outputs
515
+ * This ensures registers exist before any code generation, which is necessary
516
+ * for feedback loops where blocks may need to read from outputs that haven't
517
+ * been generated yet in the execution order
518
+ */
519
+ preallocateAllOutputs(graph, context) {
520
+ // Find all unique output ports that are connected
521
+ const connectedOutputs = new Set();
522
+ for (const connection of graph.connections) {
523
+ const key = `${connection.from.blockId}:${connection.from.portId}`;
524
+ connectedOutputs.add(key);
525
+ }
526
+ // Allocate a register for each connected output
527
+ for (const key of connectedOutputs) {
528
+ const [blockId, portId] = key.split(':');
529
+ context.allocateRegister(blockId, portId);
530
+ }
531
+ }
532
+ /**
533
+ * Process semantic IR nodes and optimize assembly
534
+ */
535
+ processIR(context) {
536
+ const sections = context.getCodeSections();
537
+ const irNodes = context.getIR();
538
+ const warnings = [];
539
+ if (irNodes.length === 0) {
540
+ return { sections, warnings };
541
+ }
542
+ // Convert IR nodes to code and group by section
543
+ const irSections = {
544
+ header: [],
545
+ init: [],
546
+ input: [],
547
+ main: [],
548
+ output: []
549
+ };
550
+ // Track accumulator state for move pruning
551
+ let accValue = null;
552
+ let lastWraxNode = null;
553
+ let optimizedCount = 0;
554
+ const optimizedNodes = [];
555
+ // Pass 1: Peephole AST Optimizer
556
+ for (const node of irNodes) {
557
+ let skipNode = false;
558
+ // Reset accumulator tracking on control flow or labels
559
+ if (node.op === 'SKP' || node.op === 'JMP' || node.op.endsWith(':')) {
560
+ accValue = null;
561
+ lastWraxNode = null;
562
+ }
563
+ // Specialized move pruning (WRAX -> LDAX optimization)
564
+ if (node.op === 'LDAX') {
565
+ const reg = (node.args[0] || '').trim();
566
+ const lastWraxMultiplier = lastWraxNode ? (lastWraxNode.args[1] || '0.0').trim() : '';
567
+ if (reg === accValue && reg !== '') {
568
+ // Accumulator already contains this register value
569
+ skipNode = true;
570
+ optimizedCount++;
571
+ }
572
+ else if (lastWraxNode && (lastWraxNode.args[0] || '').trim() === reg && (lastWraxMultiplier === '0' || lastWraxMultiplier === '0.0' || parseFloat(lastWraxMultiplier) === 0)) {
573
+ // Previous WRAX cleared ACC, but we can change it to keep ACC and prune this LDAX
574
+ lastWraxNode.args[1] = '1.0';
575
+ skipNode = true;
576
+ optimizedCount++;
577
+ accValue = reg;
578
+ }
579
+ else {
580
+ accValue = reg;
581
+ lastWraxNode = null;
582
+ }
583
+ }
584
+ else if (node.op === 'WRAX') {
585
+ const reg = (node.args[0] || '').trim();
586
+ const multiplier = (node.args[1] || '0.0').trim();
587
+ if (multiplier === '1' || multiplier === '1.0' || parseFloat(multiplier) === 1) {
588
+ accValue = reg;
589
+ }
590
+ else {
591
+ accValue = null;
592
+ }
593
+ lastWraxNode = node;
594
+ }
595
+ else if (node.op === 'WRA' || node.op === 'WRAL' || node.op === 'WRAR') {
596
+ // These clear ACC or modify it, reset tracker for safety
597
+ accValue = null;
598
+ lastWraxNode = null;
599
+ }
600
+ else if (['CLR', 'ABS', 'NEG', 'NOT'].includes(node.op)) {
601
+ accValue = null;
602
+ lastWraxNode = null;
603
+ }
604
+ else if (['RDAX', 'MAXX', 'MULX', 'RDA', 'CHO', 'SOF', 'LOG', 'EXP'].includes(node.op)) {
605
+ // Instructions that modify ACC based on a register/memory
606
+ accValue = null;
607
+ lastWraxNode = null;
608
+ }
609
+ else if (node.op !== ';') {
610
+ // Any other active instruction clears the contiguous WRAX->LDAX chain
611
+ lastWraxNode = null;
612
+ }
613
+ if (!skipNode) {
614
+ optimizedNodes.push(node);
615
+ }
616
+ }
617
+ // Serialize AST nodes into formatted string instructions
618
+ for (const node of optimizedNodes) {
619
+ // Special handling for labels (end with :) and comments (op is ;)
620
+ if (node.op.endsWith(':')) {
621
+ irSections[node.section].push(node.op);
622
+ }
623
+ else if (node.op === ';') {
624
+ // Comments should join with space, not comma
625
+ irSections[node.section].push(`;\t${node.args.join(' ')}`);
626
+ }
627
+ else {
628
+ const isDeclaration = ['EQU', 'MEM'].includes(node.op);
629
+ const separator = isDeclaration ? '\t' : ', ';
630
+ const line = `${node.op.toLowerCase()}\t${node.args.join(separator)}`;
631
+ let finalLine = line;
632
+ if (node.comment) {
633
+ finalLine += `\t; ${node.comment}`;
634
+ }
635
+ // If flattening, move everything except header/init to main
636
+ const targetSection = (context.isFlattenMode() && !['header', 'init'].includes(node.section))
637
+ ? 'main'
638
+ : node.section;
639
+ irSections[targetSection].push(finalLine);
640
+ }
641
+ }
642
+ if (optimizedCount > 0) {
643
+ warnings.push(`Pruned ${optimizedCount} redundant move instruction(s)`);
644
+ }
645
+ // Merge IR code into structured sections
646
+ const finalSections = {
647
+ header: irSections.header,
648
+ init: [...sections.init, ...irSections.init],
649
+ input: [...sections.input, ...irSections.input],
650
+ main: [...sections.main, ...irSections.main],
651
+ output: [...sections.output, ...irSections.output]
652
+ };
653
+ return { sections: finalSections, warnings };
654
+ }
655
+ }
656
+ //# sourceMappingURL=GraphCompiler.js.map