@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.
Files changed (133) 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/dist/simulator/FV1Simulator.d.ts.map +1 -1
  131. package/dist/simulator/FV1Simulator.js +7 -4
  132. package/dist/simulator/FV1Simulator.js.map +1 -1
  133. package/package.json +17 -5
@@ -0,0 +1,860 @@
1
+ /**
2
+ * BlockTemplate Engine
3
+ * Processes declarative block definitions and emits IR nodes.
4
+ */
5
+ import { AlgebraicCompiler } from '../../assembler/AlgebraicCompiler.js';
6
+ export class BlockTemplate {
7
+ constructor(definition) {
8
+ this.definition = definition;
9
+ this.algebraicCompiler = new AlgebraicCompiler();
10
+ }
11
+ /**
12
+ * Generate IR nodes for a specific block instance
13
+ */
14
+ generateIR(block, ctx) {
15
+ const ir = [];
16
+ const params = this.evaluateParameters(block, ctx);
17
+ const inputs = this.resolveInputs(block, ctx);
18
+ const outputs = this.resolveOutputs(block, ctx);
19
+ const internalRegs = this.resolveInternalRegisters(block, ctx);
20
+ const templateLines = this.definition.template.split('\n');
21
+ // Pre-process lines to remove empty lines and trim
22
+ const processedLines = templateLines.map(line => line.trim()).filter(line => line !== '');
23
+ let currentSection = 'main';
24
+ // This will be reset later
25
+ // Initial pass: find section boundaries and parse EQU declarations
26
+ for (const line of processedLines) {
27
+ if (line.startsWith('@section')) {
28
+ currentSection = line.substring(8).trim();
29
+ continue;
30
+ }
31
+ if (currentSection === 'header') {
32
+ const parts = line.split(/[,\s\t]+/);
33
+ if (parts[0].toLowerCase() === 'equ') {
34
+ let name = parts[1];
35
+ const value = parts.slice(2).join(' ');
36
+ if (name && value) {
37
+ // If name is a token, resolve it (e.g. ${local.X} -> blockId_X)
38
+ if (name.startsWith('${') && name.endsWith('}')) {
39
+ const key = name.substring(2, name.length - 1);
40
+ if (key.startsWith('local.')) {
41
+ name = `${ctx.getShortId(block.id)}_${key.split('.')[1]}`;
42
+ }
43
+ else {
44
+ name = key;
45
+ }
46
+ }
47
+ ctx.setVariable(name, value);
48
+ }
49
+ }
50
+ else if (line.startsWith('@')) {
51
+ // Handle core macros that might define variables needed for memory sizing.
52
+ // Only process calculation/variable macros in the pre-pass.
53
+ const macro = line.substring(1).split(/[,\s]+/)[0].toLowerCase();
54
+ const calculationMacros = [
55
+ 'equals', 'multiplydouble', 'multiplyint', 'dividedouble',
56
+ 'plusdouble', 'minusdouble', 'equalsbool'
57
+ ];
58
+ if (calculationMacros.includes(macro)) {
59
+ // Pass a dummy IR array to avoid duplicate instructions during the pre-pass.
60
+ this.expandMacro(line, currentSection, [], ctx, block);
61
+ }
62
+ }
63
+ }
64
+ }
65
+ // Memory must be resolved AFTER the header pre-pass to allow constants/EQU/equals
66
+ // defined in the header to be used for memory sizes.
67
+ const memory = this.resolveMemoryObjects(block, ctx);
68
+ // Reset for main pass
69
+ currentSection = 'main';
70
+ const sectionStack = [];
71
+ for (let line of processedLines) {
72
+ // line is already trimmed and filtered, so no need for:
73
+ // line = line.trim();
74
+ // if (!line || line.startsWith(';')) continue;
75
+ // Handle comments
76
+ if (line.startsWith(';') || line.startsWith('//')) {
77
+ if (sectionStack.length > 0 && sectionStack[sectionStack.length - 1].skip)
78
+ continue;
79
+ // Normalize comment character to ; and strip it for the IR node
80
+ const commentText = line.startsWith('//') ? line.substring(2) : line.substring(1);
81
+ // Still process substitutions in comments
82
+ const resolvedComment = this.performSubstitutions(commentText.trim(), params, inputs, outputs, internalRegs, memory, block, ctx);
83
+ ir.push({ op: ';', args: [resolvedComment], section: currentSection });
84
+ continue;
85
+ }
86
+ // Handle section directives
87
+ if (line.startsWith('@section')) {
88
+ const section = line.split(' ')[1];
89
+ if (['header', 'init', 'input', 'main', 'output'].includes(section)) {
90
+ currentSection = section;
91
+ }
92
+ continue;
93
+ }
94
+ // Handle @if/@else/@endif
95
+ if (line.startsWith('@if')) {
96
+ const condition = this.evaluateCondition(line.substring(3).trim(), block, ctx);
97
+ const parentSkip = sectionStack.length > 0 ? sectionStack[sectionStack.length - 1].skip : false;
98
+ sectionStack.push({
99
+ condition,
100
+ skip: parentSkip || !condition,
101
+ hasElse: false
102
+ });
103
+ continue;
104
+ }
105
+ if (line.startsWith('@else')) {
106
+ if (sectionStack.length > 0) {
107
+ const top = sectionStack[sectionStack.length - 1];
108
+ const parentSkip = sectionStack.length > 1 ? sectionStack[sectionStack.length - 2].skip : false;
109
+ top.skip = parentSkip || top.condition; // Skip else if condition was true
110
+ top.hasElse = true;
111
+ }
112
+ continue;
113
+ }
114
+ if (line.startsWith('@endif')) {
115
+ sectionStack.pop();
116
+ continue;
117
+ }
118
+ // Skip if in a false conditional branch
119
+ if (sectionStack.length > 0 && sectionStack[sectionStack.length - 1].skip) {
120
+ continue;
121
+ }
122
+ // Handle @assert directives
123
+ if (line.startsWith('@assert')) {
124
+ const assertContent = line.substring(7).trim();
125
+ const commaIdx = assertContent.indexOf(',');
126
+ if (commaIdx !== -1) {
127
+ let conditionStr = assertContent.substring(0, commaIdx).trim();
128
+ let messageStr = assertContent.substring(commaIdx + 1).trim();
129
+ // Resolve ${...} tokens in both the condition and message
130
+ conditionStr = this.performSubstitutions(conditionStr, params, inputs, outputs, internalRegs, memory, block, ctx);
131
+ messageStr = this.performSubstitutions(messageStr, params, inputs, outputs, internalRegs, memory, block, ctx);
132
+ if (messageStr.startsWith('"') && messageStr.endsWith('"')) {
133
+ messageStr = messageStr.substring(1, messageStr.length - 1);
134
+ }
135
+ else if (messageStr.startsWith("'") && messageStr.endsWith("'")) {
136
+ messageStr = messageStr.substring(1, messageStr.length - 1);
137
+ }
138
+ if (!this.evaluateCondition(conditionStr, block, ctx)) {
139
+ ctx.addError(messageStr);
140
+ }
141
+ }
142
+ else {
143
+ ctx.addError(`Invalid @assert syntax. Expected: @assert condition, "message". Got: ${line}`);
144
+ }
145
+ continue;
146
+ }
147
+ const trimmed = line.trim().toLowerCase();
148
+ const isEqu = trimmed.startsWith('equ');
149
+ const firstTokenIndex = line.indexOf('${');
150
+ const processedLine = this.performSubstitutions(line, params, inputs, outputs, internalRegs, memory, block, ctx);
151
+ // Split into code and comment (support both ; and //)
152
+ let commentIndex = processedLine.indexOf(';');
153
+ const doubleSlashIndex = processedLine.indexOf('//');
154
+ let commentMarkerLen = 1;
155
+ if (doubleSlashIndex !== -1 && (commentIndex === -1 || doubleSlashIndex < commentIndex)) {
156
+ commentIndex = doubleSlashIndex;
157
+ commentMarkerLen = 2;
158
+ }
159
+ let codePart = processedLine;
160
+ let comment = undefined;
161
+ if (commentIndex !== -1) {
162
+ codePart = processedLine.substring(0, commentIndex).trim();
163
+ comment = processedLine.substring(commentIndex + commentMarkerLen).trim();
164
+ }
165
+ // Try compiling as algebraic statement first
166
+ if (codePart.length > 0 && (codePart.includes('=') || codePart.includes('@acc') || codePart.includes('acc'))) {
167
+ const isMemoryCheck = (id) => {
168
+ // Check if identifier is in memory objects or refers to delay RAM
169
+ if (memory[id] !== undefined)
170
+ return true;
171
+ if (id.startsWith('mem.') || id.startsWith('MEM'))
172
+ return true;
173
+ if (id.toLowerCase() === 'delayl' || id.toLowerCase() === 'delayr')
174
+ return true;
175
+ return false;
176
+ };
177
+ const compiled = this.algebraicCompiler.compileLine(codePart, isMemoryCheck, (msg) => ctx.addError(msg));
178
+ if (compiled) {
179
+ codePart = compiled; // Replace codePart with the standard FV-1 assembly
180
+ }
181
+ }
182
+ // Handle Standard Macros
183
+ if (codePart.startsWith('@')) {
184
+ this.expandMacro(codePart, currentSection, ir, ctx, block);
185
+ continue;
186
+ }
187
+ // Parse assembly-like line to IR
188
+ const firstSpaceIndex = codePart.indexOf(' ');
189
+ const firstTabIndex = codePart.indexOf('\t');
190
+ let splitIndex = -1;
191
+ if (firstSpaceIndex !== -1 && firstTabIndex !== -1)
192
+ splitIndex = Math.min(firstSpaceIndex, firstTabIndex);
193
+ else if (firstSpaceIndex !== -1)
194
+ splitIndex = firstSpaceIndex;
195
+ else if (firstTabIndex !== -1)
196
+ splitIndex = firstTabIndex;
197
+ if (splitIndex !== -1) {
198
+ const op = codePart.substring(0, splitIndex).trim().toUpperCase();
199
+ const argsPart = codePart.substring(splitIndex).trim();
200
+ // Split by comma, but ignore commas inside parentheses
201
+ const args = [];
202
+ let currentArg = '';
203
+ let parenLevel = 0;
204
+ for (let i = 0; i < argsPart.length; i++) {
205
+ const char = argsPart[i];
206
+ if (char === '(')
207
+ parenLevel++;
208
+ else if (char === ')')
209
+ parenLevel--;
210
+ else if (char === ',' && parenLevel === 0) {
211
+ args.push(currentArg.trim());
212
+ currentArg = '';
213
+ continue;
214
+ }
215
+ currentArg += char;
216
+ }
217
+ if (currentArg.trim().length > 0)
218
+ args.push(currentArg.trim());
219
+ ir.push({
220
+ op,
221
+ args,
222
+ section: currentSection,
223
+ comment
224
+ });
225
+ }
226
+ else if (codePart.length > 0) {
227
+ // Opcode only
228
+ ir.push({
229
+ op: codePart.toUpperCase(),
230
+ args: [],
231
+ section: currentSection,
232
+ comment
233
+ });
234
+ }
235
+ else if (comment) {
236
+ // Just a comment line
237
+ ir.push({ op: ';', args: [comment], section: currentSection });
238
+ }
239
+ }
240
+ return ir;
241
+ }
242
+ /**
243
+ * Resolve a label template for the UI
244
+ */
245
+ resolveLabel(parameters, ctx, blockId) {
246
+ const template = this.definition.labelTemplate;
247
+ if (template === undefined)
248
+ return null;
249
+ const params = this.evaluateParametersFromMap(parameters, ctx, true);
250
+ // Prepare evaluation context
251
+ const evalCtx = {
252
+ param: params,
253
+ inputConnected: {}
254
+ };
255
+ if (ctx && blockId) {
256
+ for (const input of this.definition.inputs) {
257
+ // If we have a context, we can check if the input is connected
258
+ try {
259
+ evalCtx.inputConnected[input.id] = ctx.getInputRegister(blockId, input.id) !== undefined;
260
+ }
261
+ catch (e) {
262
+ evalCtx.inputConnected[input.id] = false;
263
+ }
264
+ }
265
+ }
266
+ return template.replace(/\$\{([^}]+)\}/g, (match, expr) => {
267
+ try {
268
+ // Safely-ish evaluate the expression using the provided context
269
+ const f = new Function('param', 'inputConnected', `return (${expr});`);
270
+ const result = f(evalCtx.param, evalCtx.inputConnected);
271
+ return result !== undefined && result !== null ? result.toString() : '';
272
+ }
273
+ catch (e) {
274
+ console.warn(`Failed to evaluate label expression: ${expr}`, e);
275
+ return match;
276
+ }
277
+ });
278
+ }
279
+ resolveValue(v, block, ctx) {
280
+ if (v === 'true')
281
+ return true;
282
+ if (v === 'false')
283
+ return false;
284
+ // 1. Check parameters
285
+ const params = this.evaluateParameters(block, ctx);
286
+ if (params[v] !== undefined)
287
+ return params[v];
288
+ // 2. Check local variables (namespaced by block short ID)
289
+ const shortId = ctx.getShortId(block.id);
290
+ const localV = ctx.getVariable(`${shortId}_${v}`);
291
+ if (localV !== undefined) {
292
+ const f = parseFloat(localV);
293
+ return isNaN(f) ? localV : f;
294
+ }
295
+ // 3. Check direct context variables
296
+ const directV = ctx.getVariable(v);
297
+ if (directV !== undefined) {
298
+ const f = parseFloat(directV);
299
+ return isNaN(f) ? directV : f;
300
+ }
301
+ // 4. Try as decimal
302
+ const f = parseFloat(v);
303
+ if (!isNaN(f))
304
+ return f;
305
+ return v;
306
+ }
307
+ evaluateCondition(condition, block, ctx) {
308
+ // Handle OR clauses (split before variable resolution to support
309
+ // pinConnected() and SpinCAD macros on either side)
310
+ const orClauses = condition.split('||');
311
+ if (orClauses.length > 1) {
312
+ return orClauses.some(clause => this.evaluateCondition(clause.trim(), block, ctx));
313
+ }
314
+ // Handle AND clauses
315
+ const andClauses = condition.split('&&');
316
+ if (andClauses.length > 1) {
317
+ return andClauses.every(clause => this.evaluateCondition(clause.trim(), block, ctx));
318
+ }
319
+ // Handle pinConnected(portId)
320
+ const pinMatch = condition.match(/pinConnected\(([^)]+)\)/);
321
+ if (pinMatch) {
322
+ let rawPinName = pinMatch[1].trim();
323
+ const wrapperMatch = rawPinName.match(/\$\{(?:input|output)\.([^}]+)\}/);
324
+ if (wrapperMatch) {
325
+ rawPinName = wrapperMatch[1];
326
+ }
327
+ let pinId = rawPinName;
328
+ const targetInput = this.definition.inputs.find(i => i.id === rawPinName || i.name === rawPinName);
329
+ if (targetInput)
330
+ pinId = targetInput.id;
331
+ else {
332
+ const targetOutput = this.definition.outputs.find(o => o.id === rawPinName || o.name === rawPinName);
333
+ if (targetOutput)
334
+ pinId = targetOutput.id;
335
+ }
336
+ return ctx.getInputRegister(block.id, pinId) !== null ||
337
+ ctx.isOutputConnected(block.id, pinId);
338
+ }
339
+ // Handle legacy SpinCAD macros used as conditions
340
+ const parts = condition.split(/\s+/);
341
+ const macro = parts[0].toLowerCase();
342
+ if (macro === 'isgreaterthan') {
343
+ return this.resolveValue(parts[1], block, ctx) > this.resolveValue(parts[2], block, ctx);
344
+ }
345
+ if (macro === 'isequalto') {
346
+ return this.resolveValue(parts[1], block, ctx) === this.resolveValue(parts[2], block, ctx);
347
+ }
348
+ if (macro === 'isor') {
349
+ const v1 = this.resolveValue(parts[1], block, ctx);
350
+ const v2 = this.resolveValue(parts[2], block, ctx);
351
+ const expected = this.resolveValue(parts[3], block, ctx);
352
+ return v1 === expected || v2 === expected;
353
+ }
354
+ // General expression evaluation:
355
+ // Resolve all variable references, then evaluate the resulting expression.
356
+ // This handles mixed variable types, arithmetic expressions, and comparisons
357
+ // like: ${param.depth} > width, (a * 2) > (b / 2), inRange > 0
358
+ const resolved = this.resolveConditionExpression(condition, block, ctx);
359
+ try {
360
+ return !!new Function(`return (${resolved});`)();
361
+ }
362
+ catch {
363
+ return false;
364
+ }
365
+ }
366
+ /**
367
+ * Resolve all variable references in a condition expression to their numeric/string values.
368
+ * Handles ${param.X}, ${local.X}, param.X, and bare identifier lookups.
369
+ * The result is a plain JavaScript expression string ready for evaluation.
370
+ */
371
+ resolveConditionExpression(expr, block, ctx) {
372
+ const params = this.evaluateParameters(block, ctx);
373
+ // 1. Replace ${param.X.max}, ${param.X.min} property access tokens
374
+ expr = expr.replace(/\$\{(?:param\.)?([^.}]+)\.(max|min)\}/g, (_, paramId, prop) => {
375
+ const paramDef = this.definition.parameters.find(p => p.id === paramId);
376
+ if (paramDef) {
377
+ let val = prop === 'max' ? paramDef.max : paramDef.min;
378
+ if (val !== undefined) {
379
+ if (paramDef.conversion && ctx) {
380
+ val = this.applyConversion(paramDef.conversion, val, ctx);
381
+ }
382
+ return String(val);
383
+ }
384
+ }
385
+ return '0';
386
+ });
387
+ // 2. Replace ${param.X} tokens (using evaluated params with conversions applied)
388
+ expr = expr.replace(/\$\{param\.([^}]+)\}/g, (_, paramName) => {
389
+ const val = params[paramName];
390
+ if (val === undefined)
391
+ return '0';
392
+ if (typeof val === 'number' || typeof val === 'boolean')
393
+ return String(val);
394
+ return JSON.stringify(String(val));
395
+ });
396
+ // 3. Replace ${local.X} tokens
397
+ expr = expr.replace(/\$\{local\.([^}]+)\}/g, (_, varName) => {
398
+ const shortId = ctx.getShortId(block.id);
399
+ const val = ctx.getVariable(`${shortId}_${varName}`);
400
+ if (val === undefined)
401
+ return '0';
402
+ const num = parseFloat(val);
403
+ return isNaN(num) ? JSON.stringify(val) : val;
404
+ });
405
+ // Helper: emit a resolved value as valid JS (numbers/booleans unquoted, strings quoted)
406
+ const toJS = (val) => {
407
+ if (typeof val === 'number' || typeof val === 'boolean')
408
+ return String(val);
409
+ return JSON.stringify(String(val));
410
+ };
411
+ // 4. Replace remaining ${X} tokens
412
+ expr = expr.replace(/\$\{([^}]+)\}/g, (match, key) => {
413
+ const val = this.resolveValue(key, block, ctx);
414
+ if (val === key)
415
+ return match; // unresolved
416
+ return toJS(val);
417
+ });
418
+ // 5. Replace bare identifiers (param names, context variables, param.X dotted refs)
419
+ // Match word characters and dots, but not numbers at the start, and not inside quotes.
420
+ expr = expr.replace(/\b([a-zA-Z_][a-zA-Z0-9_.]*)\b/g, (match) => {
421
+ // Skip JS literals
422
+ if (['true', 'false', 'null', 'undefined', 'NaN', 'Infinity'].includes(match))
423
+ return match;
424
+ // Try param.X dotted syntax
425
+ if (match.startsWith('param.')) {
426
+ const paramName = match.substring(6);
427
+ // Check for .max/.min suffix
428
+ const propMatch = paramName.match(/^(.+)\.(max|min)$/);
429
+ if (propMatch) {
430
+ const paramDef = this.definition.parameters.find(p => p.id === propMatch[1]);
431
+ if (paramDef) {
432
+ let val = propMatch[2] === 'max' ? paramDef.max : paramDef.min;
433
+ if (val !== undefined) {
434
+ if (paramDef.conversion && ctx) {
435
+ val = this.applyConversion(paramDef.conversion, val, ctx);
436
+ }
437
+ return String(val);
438
+ }
439
+ }
440
+ }
441
+ const val = params[paramName];
442
+ if (val !== undefined)
443
+ return toJS(val);
444
+ }
445
+ // Try resolveValue (checks params, local vars, context vars, numeric literal)
446
+ const val = this.resolveValue(match, block, ctx);
447
+ if (val !== match)
448
+ return toJS(val);
449
+ return match;
450
+ });
451
+ return expr;
452
+ }
453
+ expandMacro(line, section, ir, ctx, block) {
454
+ const parts = line.substring(1).split(/[,\s]+/).filter(p => p.length > 0);
455
+ const macro = parts[0].toLowerCase();
456
+ const args = parts.slice(1);
457
+ switch (macro) {
458
+ case 'equals': {
459
+ const eqName = args[0];
460
+ const eqNameIndex = line.indexOf(eqName);
461
+ let expr = args[1];
462
+ if (eqNameIndex !== -1) {
463
+ expr = line.substring(eqNameIndex + eqName.length).trim();
464
+ }
465
+ else {
466
+ expr = args.slice(1).join(' ');
467
+ }
468
+ const eqValue = this.resolveValue(expr, block, ctx);
469
+ ctx.setVariable(eqName, eqValue.toString());
470
+ break;
471
+ }
472
+ case 'multiplydouble':
473
+ case 'multiplyint': {
474
+ const mulA = this.resolveValue(args[1], block, ctx);
475
+ const mulB = this.resolveValue(args[2], block, ctx);
476
+ const mulVal = (typeof mulA === 'number' ? mulA : 0) * (typeof mulB === 'number' ? mulB : 0);
477
+ ctx.setVariable(args[0], mulVal.toString());
478
+ break;
479
+ }
480
+ case 'dividedouble': {
481
+ const divA = this.resolveValue(args[1], block, ctx);
482
+ const divB = this.resolveValue(args[2], block, ctx);
483
+ const divVal = (typeof divA === 'number' ? divA : 0) / (typeof divB === 'number' ? divB : 1);
484
+ ctx.setVariable(args[0], divVal.toString());
485
+ break;
486
+ }
487
+ case 'plusdouble': {
488
+ const pA = this.resolveValue(args[1], block, ctx);
489
+ const pB = this.resolveValue(args[2], block, ctx);
490
+ const pVal = (typeof pA === 'number' ? pA : 0) + (typeof pB === 'number' ? pB : 0);
491
+ ctx.setVariable(args[0], pVal.toString());
492
+ break;
493
+ }
494
+ case 'minusdouble': {
495
+ const mA = this.resolveValue(args[1], block, ctx);
496
+ const mB = this.resolveValue(args[2], block, ctx);
497
+ const mVal = (typeof mA === 'number' ? mA : 0) - (typeof mB === 'number' ? mB : 0);
498
+ ctx.setVariable(args[0], mVal.toString());
499
+ break;
500
+ }
501
+ case 'isgreaterthan':
502
+ // @isGreaterThan a, b
503
+ if (parseFloat(args[0]) > parseFloat(args[1])) {
504
+ // This is tricky because @isGreaterThan is usually followed by @endif
505
+ // but it's handled by the parent line-by-line processor for @if.
506
+ // Actually, SpinCAD uses it like an @if.
507
+ // We should probably have handled this in generateIR's loop.
508
+ }
509
+ break;
510
+ case 'isequaltobool':
511
+ case 'equalsbool':
512
+ // @equalsBool var, val
513
+ ctx.setVariable(args[0], args[1]);
514
+ break;
515
+ case 'cv': {
516
+ // @cv portId
517
+ // Reads a control input, using the port's associated parameter as the range/default.
518
+ // - Unconnected: SOF 0.0, equName → ACC = paramValue (constant)
519
+ // - Connected: CLR; SOF 0.0, equName; MULX reg → ACC ∈ [0..paramValue]
520
+ // (CLR is omitted if the previous instruction already set ACC = 0)
521
+ // - Connected + zero-bypassed: 5-instruction bypass adapted for range scaling.
522
+ // RDAX reads pot scaled by paramValue directly (no separate MULX needed).
523
+ // When pot < threshold: loads zeroValScaled (pot's zeroValue * paramValue).
524
+ const portId = args[0];
525
+ if (!portId) {
526
+ ctx.addError(`@cv requires a port ID argument`);
527
+ break;
528
+ }
529
+ // Get the associated parameter from the input definition
530
+ const inputDef = this.definition.inputs.find(i => i.id === portId);
531
+ const paramId = inputDef?.parameter;
532
+ if (!paramId) {
533
+ ctx.addError(`@cv: input port '${portId}' has no 'parameter' field defined in the block JSON`);
534
+ break;
535
+ }
536
+ // Evaluate parameters (with conversions applied)
537
+ const cvParams = this.evaluateParameters(block, ctx);
538
+ const paramValue = cvParams[paramId];
539
+ if (paramValue === undefined || paramValue === null) {
540
+ ctx.addError(`@cv: parameter '${paramId}' not found on block`);
541
+ break;
542
+ }
543
+ // EQU name for the range constant
544
+ const shortId = ctx.getShortId(block.id);
545
+ const equName = `cv_${shortId}_${portId}`;
546
+ // Push EQU to IR header section only (idempotent), NOT to equDeclarations.
547
+ // Using registerHeaderEqu avoids duplicating the EQU in the init/Constants block.
548
+ if (!ctx.hasHeaderEqu(equName)) {
549
+ ctx.pushIR({ op: 'EQU', args: [equName, paramValue.toString()], section: 'header' });
550
+ ctx.registerHeaderEqu(equName);
551
+ }
552
+ const reg = ctx.getInputRegister(block.id, portId);
553
+ const isZeroBypassed = ctx.isInputZeroBypassed(block.id, portId);
554
+ if (reg) {
555
+ if (isZeroBypassed) {
556
+ const zeroVal = ctx.getInputZeroValue(block.id, portId);
557
+ ir.push({ op: 'RDAX', args: [reg, '1.0'], section, comment: `Read pot` });
558
+ ir.push({ op: 'SOF', args: ['1.0', '-0.02'], section, comment: 'Subtract bypass threshold' });
559
+ ir.push({ op: 'SKP', args: ['GEZ', '1'], section, comment: 'Skip if pot >= threshold' });
560
+ ir.push({ op: 'SOF', args: ['0.0', `${(zeroVal - 0.02).toFixed(6)}`], section, comment: `Zero fallback` });
561
+ ir.push({ op: 'SOF', args: ['1.0', '0.02'], section, comment: 'Re-add threshold' });
562
+ ir.push({ op: 'SOF', args: [equName, '0.0'], section, comment: `Scale CV by ${paramId}` });
563
+ }
564
+ else {
565
+ // Standard connected: CLR (if needed), load range, scale by pot.
566
+ // Check if ACC is already 0 from the previous instruction so we can skip the CLR.
567
+ const prevNode = [...ir].reverse().find(n => n.op !== ';');
568
+ const prevClearsAcc = prevNode && (prevNode.op === 'CLR' ||
569
+ (prevNode.op === 'WRAX' && (prevNode.args[1] === '0.0' || prevNode.args[1] === '0' || parseFloat(prevNode.args[1]) === 0)) ||
570
+ (prevNode.op === 'WRA' && (prevNode.args[1] === '0.0' || prevNode.args[1] === '0' || parseFloat(prevNode.args[1]) === 0)) ||
571
+ (prevNode.op === 'WRHX' && (prevNode.args[1] === '0.0' || prevNode.args[1] === '0' || parseFloat(prevNode.args[1]) === 0)));
572
+ if (!prevClearsAcc) {
573
+ ir.push({ op: 'CLR', args: [], section });
574
+ }
575
+ ir.push({ op: 'SOF', args: ['0.0', equName], section, comment: `CV range: ${paramId}` });
576
+ ir.push({ op: 'MULX', args: [reg], section });
577
+ }
578
+ }
579
+ else {
580
+ // Unconnected: use parameter value as a constant
581
+ ir.push({ op: 'SOF', args: ['0.0', equName], section, comment: `CV default: ${paramId}` });
582
+ }
583
+ break;
584
+ }
585
+ case 'mulcv': {
586
+ // @mulcv portId
587
+ // Scale the current ACC by the CV value without clearing first.
588
+ // Assumes ACC already contains a signal to be modulated.
589
+ // - Unconnected: SOF paramValue, 0.0 → ACC = ACC * paramValue (1 instruction)
590
+ // - Connected: SOF paramValue, 0.0; MULX reg → ACC = ACC * paramValue * pot (2 instructions)
591
+ // - Connected + zero-bypassed: same bypass pattern as @cv but applied to existing ACC
592
+ const mulcvPortId = args[0];
593
+ if (!mulcvPortId) {
594
+ ctx.addError(`@mulcv requires a port ID argument`);
595
+ break;
596
+ }
597
+ const mulcvInputDef = this.definition.inputs.find(i => i.id === mulcvPortId);
598
+ const mulcvParamId = mulcvInputDef?.parameter;
599
+ if (!mulcvParamId) {
600
+ ctx.addError(`@mulcv: input port '${mulcvPortId}' has no 'parameter' field defined in the block JSON`);
601
+ break;
602
+ }
603
+ const mulcvParams = this.evaluateParameters(block, ctx);
604
+ const mulcvParamValue = mulcvParams[mulcvParamId];
605
+ if (mulcvParamValue === undefined || mulcvParamValue === null) {
606
+ ctx.addError(`@mulcv: parameter '${mulcvParamId}' not found on block`);
607
+ break;
608
+ }
609
+ const mulcvShortId = ctx.getShortId(block.id);
610
+ const mulcvEquName = `cv_${mulcvShortId}_${mulcvPortId}`;
611
+ // Shared EQU registration with @cv (idempotent)
612
+ if (!ctx.hasHeaderEqu(mulcvEquName)) {
613
+ ctx.pushIR({ op: 'EQU', args: [mulcvEquName, mulcvParamValue.toString()], section: 'header' });
614
+ ctx.registerHeaderEqu(mulcvEquName);
615
+ }
616
+ const mulcvReg = ctx.getInputRegister(block.id, mulcvPortId);
617
+ const mulcvZeroBypassed = ctx.isInputZeroBypassed(block.id, mulcvPortId);
618
+ if (mulcvReg) {
619
+ if (mulcvZeroBypassed) {
620
+ const scratch = ctx.getScratchRegister();
621
+ const mulcvZeroVal = ctx.getInputZeroValue(block.id, mulcvPortId);
622
+ ir.push({ op: 'WRAX', args: [scratch, '0.0'], section, comment: 'Save ACC for @mulcv bypass' });
623
+ ir.push({ op: 'RDAX', args: [mulcvReg, '1.0'], section, comment: `Read pot` });
624
+ ir.push({ op: 'SOF', args: ['1.0', '-0.02'], section, comment: 'Subtract bypass threshold' });
625
+ ir.push({ op: 'SKP', args: ['GEZ', '1'], section, comment: 'Skip if pot >= threshold' });
626
+ ir.push({ op: 'SOF', args: ['0.0', `${(mulcvZeroVal - 0.02).toFixed(6)}`], section, comment: `Zero fallback` });
627
+ ir.push({ op: 'SOF', args: ['1.0', '0.02'], section, comment: 'Re-add threshold' });
628
+ ir.push({ op: 'MULX', args: [scratch], section, comment: 'Scale saved signal by CV' });
629
+ ir.push({ op: 'SOF', args: [mulcvEquName, '0.0'], section, comment: `Scale by ${mulcvParamId}` });
630
+ }
631
+ else {
632
+ // Connected: scale ACC by paramValue, then scale by pot
633
+ ir.push({ op: 'SOF', args: [mulcvEquName, '0.0'], section, comment: `@mulcv: scale by ${mulcvParamId}` });
634
+ ir.push({ op: 'MULX', args: [mulcvReg], section, comment: 'Scale by pot' });
635
+ }
636
+ }
637
+ else {
638
+ // Unconnected: scale ACC by the parameter constant
639
+ ir.push({ op: 'SOF', args: [mulcvEquName, '0.0'], section, comment: `@mulcv default: ${mulcvParamId}` });
640
+ }
641
+ break;
642
+ }
643
+ case 'readchorustap':
644
+ // @readChorusTap lfoSel flags center length offset
645
+ const lfoIdx = args[0] === '0' ? 'SIN0' : 'SIN1';
646
+ const tapFlags = args[1] === '0' ? 'REG|COMPC' : args[1]; // simplified
647
+ const tapMemo = args[4]; // assuming memory ID
648
+ ir.push({ op: 'CHO', args: ['RDA', lfoIdx, tapFlags, tapMemo], section });
649
+ ir.push({ op: 'CHO', args: ['RDA', lfoIdx, '0', `${tapMemo}+1`], section });
650
+ break;
651
+ case 'getbaseaddress':
652
+ // usually a no-op in our managed system
653
+ break;
654
+ case 'gain':
655
+ // @gain result, input, gain
656
+ ir.push({ op: 'RDAX', args: [args[1], args[2]], section });
657
+ break;
658
+ case 'comment':
659
+ // @comment text
660
+ const text = args.join(' ');
661
+ ir.push({ op: ';', args: [text], section: 'init' });
662
+ break;
663
+ default:
664
+ ctx.addError(`Unsupported directive or macro: @${macro}`);
665
+ break;
666
+ }
667
+ }
668
+ evaluateParameters(block, ctx) {
669
+ return this.evaluateParametersFromMap(block.parameters, ctx);
670
+ }
671
+ evaluateParametersFromMap(parameters, ctx, skipConversion = false) {
672
+ const evaluated = {};
673
+ for (const param of this.definition.parameters) {
674
+ let val = parameters[param.id] ?? param.default;
675
+ // Apply modern conversions
676
+ if (param.conversion && ctx && !skipConversion) {
677
+ val = this.applyConversion(param.conversion, val, ctx);
678
+ }
679
+ evaluated[param.id] = val;
680
+ }
681
+ return evaluated;
682
+ }
683
+ applyConversion(type, val, ctx) {
684
+ const Fs = 32768; // Default, should be pulled from context if dynamic
685
+ switch (type) {
686
+ case 'LOGFREQ':
687
+ return (1.0 - Math.exp(-2.0 * Math.PI * val / Fs)).toFixed(6);
688
+ case 'SVFFREQ':
689
+ // Returns sin(2 * pi * f / Fs), matching SpinCAD's base frequency coefficient
690
+ return (Math.sin(2.0 * Math.PI * val / Fs)).toFixed(6);
691
+ case 'SVF_DAMP':
692
+ // Returns 1.0 / Q (Chamberlin damping d). Used for Min/Max bounds.
693
+ return (1.0 / val).toFixed(6);
694
+ case 'SINLFOFREQ':
695
+ case 'HZ_TO_LFO_RATE':
696
+ return Math.round((1 << 18) * Math.PI * val / Fs);
697
+ case 'DBLEVEL':
698
+ return Math.pow(10.0, val / 20.0).toFixed(6);
699
+ case 'LENGTHTOTIME':
700
+ case 'MS_TO_SAMPLES':
701
+ return Math.round((val * Fs / 1000));
702
+ case 'MS_TO_LFO_RANGE':
703
+ return Math.round((val * Fs / 1000) * 32767 / 16385);
704
+ case 'SEMITONES_TO_RATE':
705
+ return ((Math.pow(2, val / 12) - 1.0) * 16384).toFixed(6);
706
+ case 'HZ_TO_HILBERT_SHIFT':
707
+ return (2 * Math.PI * val / Fs).toFixed(6);
708
+ default:
709
+ return typeof val === 'number' ? val.toFixed(6) : val;
710
+ }
711
+ }
712
+ resolveInputs(block, ctx) {
713
+ const resolved = {};
714
+ for (const input of this.definition.inputs) {
715
+ const reg = ctx.getInputRegister(block.id, input.id);
716
+ if (reg)
717
+ resolved[input.id] = reg;
718
+ }
719
+ return resolved;
720
+ }
721
+ resolveOutputs(block, ctx) {
722
+ const resolved = {};
723
+ for (const output of this.definition.outputs) {
724
+ // Only allocate a register if this output is connected downstream
725
+ if (ctx.isOutputConnected(block.id, output.id)) {
726
+ const reg = ctx.allocateRegister(block.id, output.id);
727
+ resolved[output.id] = reg;
728
+ }
729
+ }
730
+ return resolved;
731
+ }
732
+ resolveInternalRegisters(block, ctx) {
733
+ const resolved = {};
734
+ if (this.definition.registers) {
735
+ for (const regId of this.definition.registers) {
736
+ resolved[regId] = ctx.allocateRegister(block.id, `local_${regId}`);
737
+ }
738
+ }
739
+ return resolved;
740
+ }
741
+ resolveMemoryObjects(block, ctx) {
742
+ const resolved = {};
743
+ if (this.definition.memories) {
744
+ const params = this.evaluateParameters(block, ctx);
745
+ for (const mem of this.definition.memories) {
746
+ let size = 0;
747
+ if (typeof mem.size === 'string' || typeof mem.size === 'number') {
748
+ const sizeStr = mem.size.toString();
749
+ // If the size is a raw parameter name without interpolation (e.g. "preDelay" vs "${preDelay}"), wrap it natively.
750
+ // This handles old-style ATL blocks that define memory size as just the string identifier.
751
+ const evalStr = sizeStr.includes('${') ? sizeStr : /^[_a-zA-Z][_a-zA-Z0-9]*$/.test(sizeStr) ? `\${${sizeStr}}` : sizeStr;
752
+ // Substitute parameter variables first (e.g. "${delayLength} * 0.5")
753
+ const substitutedSizeExpr = this.performSubstitutions(evalStr, params, {}, {}, {}, {}, block, ctx);
754
+ if (substitutedSizeExpr.trim() === '') {
755
+ ctx.addError(`Memory size expression '${sizeStr}' evaluated to empty.`);
756
+ size = 1;
757
+ }
758
+ else {
759
+ const evalResult = this.algebraicCompiler.evaluateConstantExpression(substitutedSizeExpr);
760
+ if (evalResult !== null) {
761
+ if (isNaN(evalResult) || evalResult < 0) {
762
+ ctx.addError(`Memory size expression '${sizeStr}' evaluated to invalid number: ${evalResult}`);
763
+ size = 1;
764
+ }
765
+ else {
766
+ size = Math.round(evalResult);
767
+ }
768
+ }
769
+ else {
770
+ size = parseInt(substitutedSizeExpr, 10);
771
+ if (isNaN(size) || size < 0) {
772
+ ctx.addError(`Memory size expression '${sizeStr}' could not be parsed as a valid size.`);
773
+ size = 1;
774
+ }
775
+ }
776
+ }
777
+ }
778
+ else {
779
+ size = mem.size;
780
+ }
781
+ const alloc = ctx.allocateMemory(mem.id, size);
782
+ resolved[mem.id] = alloc;
783
+ }
784
+ }
785
+ return resolved;
786
+ }
787
+ performSubstitutions(line, params, inputs, outputs, internalRegs, memory, block, ctx) {
788
+ const trimmed = line.trim().toLowerCase();
789
+ const isEqu = trimmed.startsWith('equ');
790
+ const firstTokenIndex = line.indexOf('${');
791
+ return line.replace(/\$\{([^}]+)\}/g, (match, key, offset) => {
792
+ // Check for property modifiers like .max and .min
793
+ const propMatch = key.match(/^(?:param\.)?([^.]+)\.(max|min)$/);
794
+ if (propMatch) {
795
+ const paramId = propMatch[1];
796
+ const prop = propMatch[2];
797
+ const paramDef = this.definition.parameters.find(p => p.id === paramId);
798
+ if (paramDef) {
799
+ let val = prop === 'max' ? paramDef.max : paramDef.min;
800
+ if (val !== undefined) {
801
+ if (paramDef.conversion && ctx) {
802
+ val = this.applyConversion(paramDef.conversion, val, ctx);
803
+ }
804
+ return val?.toString() ?? '';
805
+ }
806
+ }
807
+ }
808
+ if (key.startsWith('param.'))
809
+ return params[key.split('.')[1]]?.toString() || '';
810
+ if (key.startsWith('input.'))
811
+ return inputs[key.split('.')[1]] || '';
812
+ if (key.startsWith('output.'))
813
+ return outputs[key.split('.')[1]] || '';
814
+ if (key.startsWith('reg.'))
815
+ return internalRegs[key.split('.')[1]] || '';
816
+ if (key === 'Fs' || key === 'samplingRate')
817
+ return '32768';
818
+ if (key.startsWith('mem.')) {
819
+ const parts = key.split('.');
820
+ const memId = parts[1];
821
+ const prop = parts[2];
822
+ const alloc = memory[memId];
823
+ if (!alloc)
824
+ return '';
825
+ if (!prop || prop === 'start')
826
+ return alloc.name;
827
+ if (prop === 'size')
828
+ return alloc.size.toString();
829
+ if (prop === 'end')
830
+ return `(${alloc.name} + ${alloc.size} - 1)`;
831
+ if (prop === 'middle')
832
+ return `(${alloc.name} + ${alloc.size} / 2)`;
833
+ return alloc.name;
834
+ }
835
+ if (key.startsWith('local.')) {
836
+ const shortId = ctx.getShortId(block.id);
837
+ const varName = `${shortId}_${key.split('.')[1]}`;
838
+ // If this is the definition part of an EQU line, return the name only
839
+ if (isEqu && offset === firstTokenIndex) {
840
+ return varName;
841
+ }
842
+ const val = ctx.getVariable(varName);
843
+ return val !== undefined ? val : varName;
844
+ }
845
+ // Check direct parameter names
846
+ if (params[key] !== undefined)
847
+ return params[key].toString();
848
+ // Check dynamic variables set by macros
849
+ const v = ctx.getVariable(key);
850
+ if (v !== undefined)
851
+ return v;
852
+ // Check if it's a raw local name that was already expanded
853
+ const localV = ctx.getVariable(`${block.id}_${key}`);
854
+ if (localV !== undefined)
855
+ return localV;
856
+ return match;
857
+ });
858
+ }
859
+ }
860
+ //# sourceMappingURL=BlockTemplate.js.map