@audiofab-io/fv1-core 0.2.2 → 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.
- package/blocks/ATL_DEVELOPER_REFERENCE.md +156 -0
- package/blocks/control/constant.atl +36 -0
- package/blocks/control/entropy_lfo.atl +74 -0
- package/blocks/control/envelope.atl +121 -0
- package/blocks/control/invert.atl +33 -0
- package/blocks/control/pot.atl +150 -0
- package/blocks/control/power.atl +77 -0
- package/blocks/control/ramp_lfo.atl +122 -0
- package/blocks/control/scale_offset.atl +84 -0
- package/blocks/control/sincos_lfo.atl +126 -0
- package/blocks/control/smoother.atl +48 -0
- package/blocks/control/tremolizer.atl +55 -0
- package/blocks/effects/delay/micro_stutter.atl +77 -0
- package/blocks/effects/delay/mn3011.atl +281 -0
- package/blocks/effects/delay/simple_delay.atl +96 -0
- package/blocks/effects/delay/triple_tap_delay.atl +176 -0
- package/blocks/effects/lo-fi/bit_mangler.atl +74 -0
- package/blocks/effects/lo-fi/chiptune.atl +311 -0
- package/blocks/effects/lo-fi/tape_degrade.atl +181 -0
- package/blocks/effects/modulation/chorus.atl +141 -0
- package/blocks/effects/modulation/chorus_4voice.atl +188 -0
- package/blocks/effects/modulation/flanger.atl +184 -0
- package/blocks/effects/modulation/guitar_synth.atl +350 -0
- package/blocks/effects/modulation/harmonic_trem.atl +129 -0
- package/blocks/effects/modulation/organ_synth.atl +326 -0
- package/blocks/effects/modulation/phaser.atl +300 -0
- package/blocks/effects/pitch/octave_up_down.atl +80 -0
- package/blocks/effects/pitch/pitch_offset.atl +149 -0
- package/blocks/effects/pitch/pitch_offset_dual.atl +197 -0
- package/blocks/effects/pitch/pitch_shift.atl +115 -0
- package/blocks/effects/pitch/sub_octave.atl +100 -0
- package/blocks/effects/reverb/ducking_reverb.atl +145 -0
- package/blocks/effects/reverb/min_reverb.atl +132 -0
- package/blocks/effects/reverb/plate_reverb.atl +344 -0
- package/blocks/effects/reverb/room_reverb.atl +293 -0
- package/blocks/effects/reverb/smear.atl +90 -0
- package/blocks/effects/reverb/spring_reverb.atl +353 -0
- package/blocks/filter/1p_high_pass.atl +63 -0
- package/blocks/filter/1p_low_pass.atl +59 -0
- package/blocks/filter/auto_wah.atl +207 -0
- package/blocks/filter/bbd_loss.atl +79 -0
- package/blocks/filter/shelving_high_pass.atl +76 -0
- package/blocks/filter/shelving_low_pass.atl +76 -0
- package/blocks/filter/svf_2p.atl +116 -0
- package/blocks/gain_mix/crossfade.atl +93 -0
- package/blocks/gain_mix/crossfade2.atl +86 -0
- package/blocks/gain_mix/crossfade3.atl +71 -0
- package/blocks/gain_mix/gainboost.atl +54 -0
- package/blocks/gain_mix/mixer2.atl +76 -0
- package/blocks/gain_mix/mixer3.atl +109 -0
- package/blocks/gain_mix/mixer4.atl +152 -0
- package/blocks/gain_mix/volume.atl +51 -0
- package/blocks/io/adc.atl +53 -0
- package/blocks/io/dac.atl +61 -0
- package/blocks/other/stickynote.atl +24 -0
- package/blocks/other/tone_gen_adjustable.atl +137 -0
- package/blocks/other/tone_gen_fixed.atl +109 -0
- package/dist/blockDiagram/blocks/BlockDirectoryLoader.d.ts +13 -0
- package/dist/blockDiagram/blocks/BlockDirectoryLoader.d.ts.map +1 -0
- package/dist/blockDiagram/blocks/BlockDirectoryLoader.js +44 -0
- package/dist/blockDiagram/blocks/BlockDirectoryLoader.js.map +1 -0
- package/dist/blockDiagram/blocks/BlockRegistry.d.ts +48 -0
- package/dist/blockDiagram/blocks/BlockRegistry.d.ts.map +1 -0
- package/dist/blockDiagram/blocks/BlockRegistry.js +109 -0
- package/dist/blockDiagram/blocks/BlockRegistry.js.map +1 -0
- package/dist/blockDiagram/blocks/TemplateBlock.d.ts +20 -0
- package/dist/blockDiagram/blocks/TemplateBlock.d.ts.map +1 -0
- package/dist/blockDiagram/blocks/TemplateBlock.js +82 -0
- package/dist/blockDiagram/blocks/TemplateBlock.js.map +1 -0
- package/dist/blockDiagram/blocks/base/BaseBlock.d.ts +248 -0
- package/dist/blockDiagram/blocks/base/BaseBlock.d.ts.map +1 -0
- package/dist/blockDiagram/blocks/base/BaseBlock.js +402 -0
- package/dist/blockDiagram/blocks/base/BaseBlock.js.map +1 -0
- package/dist/blockDiagram/builtinBlocks.d.ts +9 -0
- package/dist/blockDiagram/builtinBlocks.d.ts.map +1 -0
- package/dist/blockDiagram/builtinBlocks.js +4912 -0
- package/dist/blockDiagram/builtinBlocks.js.map +1 -0
- package/dist/blockDiagram/compiler/BlockTemplate.d.ts +37 -0
- package/dist/blockDiagram/compiler/BlockTemplate.d.ts.map +1 -0
- package/dist/blockDiagram/compiler/BlockTemplate.js +860 -0
- package/dist/blockDiagram/compiler/BlockTemplate.js.map +1 -0
- package/dist/blockDiagram/compiler/CodeOptimizer.d.ts +75 -0
- package/dist/blockDiagram/compiler/CodeOptimizer.d.ts.map +1 -0
- package/dist/blockDiagram/compiler/CodeOptimizer.js +443 -0
- package/dist/blockDiagram/compiler/CodeOptimizer.js.map +1 -0
- package/dist/blockDiagram/compiler/GraphCompiler.d.ts +63 -0
- package/dist/blockDiagram/compiler/GraphCompiler.d.ts.map +1 -0
- package/dist/blockDiagram/compiler/GraphCompiler.js +656 -0
- package/dist/blockDiagram/compiler/GraphCompiler.js.map +1 -0
- package/dist/blockDiagram/compiler/TopologicalSort.d.ts +63 -0
- package/dist/blockDiagram/compiler/TopologicalSort.d.ts.map +1 -0
- package/dist/blockDiagram/compiler/TopologicalSort.js +268 -0
- package/dist/blockDiagram/compiler/TopologicalSort.js.map +1 -0
- package/dist/blockDiagram/index.d.ts +30 -0
- package/dist/blockDiagram/index.d.ts.map +1 -0
- package/dist/blockDiagram/index.js +29 -0
- package/dist/blockDiagram/index.js.map +1 -0
- package/dist/blockDiagram/types/Block.d.ts +178 -0
- package/dist/blockDiagram/types/Block.d.ts.map +1 -0
- package/dist/blockDiagram/types/Block.js +5 -0
- package/dist/blockDiagram/types/Block.js.map +1 -0
- package/dist/blockDiagram/types/CodeGenContext.d.ts +235 -0
- package/dist/blockDiagram/types/CodeGenContext.d.ts.map +1 -0
- package/dist/blockDiagram/types/CodeGenContext.js +554 -0
- package/dist/blockDiagram/types/CodeGenContext.js.map +1 -0
- package/dist/blockDiagram/types/Connection.d.ts +17 -0
- package/dist/blockDiagram/types/Connection.d.ts.map +1 -0
- package/dist/blockDiagram/types/Connection.js +5 -0
- package/dist/blockDiagram/types/Connection.js.map +1 -0
- package/dist/blockDiagram/types/Graph.d.ts +28 -0
- package/dist/blockDiagram/types/Graph.d.ts.map +1 -0
- package/dist/blockDiagram/types/Graph.js +24 -0
- package/dist/blockDiagram/types/Graph.js.map +1 -0
- package/dist/blockDiagram/types/IR.d.ts +79 -0
- package/dist/blockDiagram/types/IR.d.ts.map +1 -0
- package/dist/blockDiagram/types/IR.js +6 -0
- package/dist/blockDiagram/types/IR.js.map +1 -0
- package/dist/blockDiagram/utils/SpinCADConverter.d.ts +17 -0
- package/dist/blockDiagram/utils/SpinCADConverter.d.ts.map +1 -0
- package/dist/blockDiagram/utils/SpinCADConverter.js +307 -0
- package/dist/blockDiagram/utils/SpinCADConverter.js.map +1 -0
- package/dist/effect/compileEffect.d.ts +51 -0
- package/dist/effect/compileEffect.d.ts.map +1 -0
- package/dist/effect/compileEffect.js +133 -0
- package/dist/effect/compileEffect.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/simulator/FV1Simulator.d.ts.map +1 -1
- package/dist/simulator/FV1Simulator.js +7 -4
- package/dist/simulator/FV1Simulator.js.map +1 -1
- 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
|