frausto 0.2.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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +45 -0
- data/bin/faust2ruby +124 -0
- data/bin/ruby2faust +129 -0
- data/faust2ruby.md +523 -0
- data/lib/faust2ruby/ast.rb +315 -0
- data/lib/faust2ruby/ir_builder.rb +413 -0
- data/lib/faust2ruby/lexer.rb +255 -0
- data/lib/faust2ruby/library_mapper.rb +249 -0
- data/lib/faust2ruby/parser.rb +596 -0
- data/lib/faust2ruby/ruby_generator.rb +708 -0
- data/lib/faust2ruby/version.rb +5 -0
- data/lib/faust2ruby.rb +82 -0
- data/lib/frausto.rb +8 -0
- data/lib/ruby2faust/dsl.rb +1332 -0
- data/lib/ruby2faust/emitter.rb +599 -0
- data/lib/ruby2faust/ir.rb +285 -0
- data/lib/ruby2faust/live.rb +82 -0
- data/lib/ruby2faust/version.rb +5 -0
- data/lib/ruby2faust.rb +27 -0
- data/ruby2faust.md +334 -0
- metadata +106 -0
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ast"
|
|
4
|
+
require_relative "library_mapper"
|
|
5
|
+
|
|
6
|
+
module Faust2Ruby
|
|
7
|
+
# Generates idiomatic Ruby DSL code from Faust AST.
|
|
8
|
+
# Produces code compatible with the ruby2faust gem.
|
|
9
|
+
class RubyGenerator
|
|
10
|
+
def initialize(options = {})
|
|
11
|
+
@indent = options.fetch(:indent, 2)
|
|
12
|
+
@expression_only = options.fetch(:expression_only, false)
|
|
13
|
+
@definitions = {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Generate Ruby code from a parsed Faust program
|
|
17
|
+
def generate(program)
|
|
18
|
+
lines = []
|
|
19
|
+
|
|
20
|
+
# Merge multi-rule functions into case expressions
|
|
21
|
+
merged_statements = merge_multirule_functions(program.statements)
|
|
22
|
+
|
|
23
|
+
# Collect definitions for reference
|
|
24
|
+
merged_statements.each do |stmt|
|
|
25
|
+
@definitions[stmt.name] = stmt if stmt.is_a?(AST::Definition)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Collect imports and declares
|
|
29
|
+
imports = program.statements.select { |s| s.is_a?(AST::Import) }.map(&:path)
|
|
30
|
+
declares = program.statements.select { |s| s.is_a?(AST::Declare) }
|
|
31
|
+
|
|
32
|
+
unless @expression_only
|
|
33
|
+
lines << "require 'ruby2faust'"
|
|
34
|
+
lines << "include Ruby2Faust::DSL"
|
|
35
|
+
lines << ""
|
|
36
|
+
|
|
37
|
+
# Generate declares as comments
|
|
38
|
+
declares.each do |stmt|
|
|
39
|
+
lines << "# declare #{stmt.key} \"#{stmt.value}\""
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
lines << "" if declares.any?
|
|
43
|
+
|
|
44
|
+
# Generate helper definitions (excluding process)
|
|
45
|
+
merged_statements.each do |stmt|
|
|
46
|
+
if stmt.is_a?(AST::Definition) && stmt.name != "process"
|
|
47
|
+
lines << generate_definition(stmt)
|
|
48
|
+
lines << ""
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Generate process
|
|
54
|
+
process_def = @definitions["process"]
|
|
55
|
+
if process_def
|
|
56
|
+
if @expression_only
|
|
57
|
+
lines << generate_expression(process_def.expression)
|
|
58
|
+
else
|
|
59
|
+
lines << "process = #{generate_expression(process_def.expression)}"
|
|
60
|
+
lines << ""
|
|
61
|
+
|
|
62
|
+
# Build program with imports and declares
|
|
63
|
+
lines << "prog = Ruby2Faust::Program.new(process)"
|
|
64
|
+
imports.each do |imp|
|
|
65
|
+
lines << " .import(#{imp.inspect})" unless imp == "stdfaust.lib"
|
|
66
|
+
end
|
|
67
|
+
declares.each do |d|
|
|
68
|
+
lines << " .declare(:#{d.key}, #{d.value.inspect})"
|
|
69
|
+
end
|
|
70
|
+
lines << ""
|
|
71
|
+
lines << "puts Ruby2Faust::Emitter.program(prog)"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
lines.join("\n")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Generate just the expression part (for embedding)
|
|
79
|
+
def generate_expression(node)
|
|
80
|
+
case node
|
|
81
|
+
when AST::IntLiteral
|
|
82
|
+
node.value.to_s
|
|
83
|
+
|
|
84
|
+
when AST::FloatLiteral
|
|
85
|
+
node.value.to_s
|
|
86
|
+
|
|
87
|
+
when AST::StringLiteral
|
|
88
|
+
node.value.inspect
|
|
89
|
+
|
|
90
|
+
when AST::Wire
|
|
91
|
+
"wire"
|
|
92
|
+
|
|
93
|
+
when AST::Cut
|
|
94
|
+
"cut"
|
|
95
|
+
|
|
96
|
+
when AST::Identifier
|
|
97
|
+
generate_identifier(node)
|
|
98
|
+
|
|
99
|
+
when AST::QualifiedName
|
|
100
|
+
generate_qualified_name(node)
|
|
101
|
+
|
|
102
|
+
when AST::BinaryOp
|
|
103
|
+
generate_binary_op(node)
|
|
104
|
+
|
|
105
|
+
when AST::UnaryOp
|
|
106
|
+
generate_unary_op(node)
|
|
107
|
+
|
|
108
|
+
when AST::FunctionCall
|
|
109
|
+
generate_function_call(node)
|
|
110
|
+
|
|
111
|
+
when AST::UIElement
|
|
112
|
+
generate_ui_element(node)
|
|
113
|
+
|
|
114
|
+
when AST::UIGroup
|
|
115
|
+
generate_ui_group(node)
|
|
116
|
+
|
|
117
|
+
when AST::Iteration
|
|
118
|
+
generate_iteration(node)
|
|
119
|
+
|
|
120
|
+
when AST::Lambda
|
|
121
|
+
generate_lambda(node)
|
|
122
|
+
|
|
123
|
+
when AST::Waveform
|
|
124
|
+
generate_waveform(node)
|
|
125
|
+
|
|
126
|
+
when AST::Table
|
|
127
|
+
generate_table(node)
|
|
128
|
+
|
|
129
|
+
when AST::Route
|
|
130
|
+
generate_route(node)
|
|
131
|
+
|
|
132
|
+
when AST::Prime
|
|
133
|
+
generate_prime(node)
|
|
134
|
+
|
|
135
|
+
when AST::Access
|
|
136
|
+
generate_access(node)
|
|
137
|
+
|
|
138
|
+
when AST::Paren
|
|
139
|
+
"(#{generate_expression(node.expression)})"
|
|
140
|
+
|
|
141
|
+
when AST::With
|
|
142
|
+
generate_with(node)
|
|
143
|
+
|
|
144
|
+
when AST::Letrec
|
|
145
|
+
generate_letrec(node)
|
|
146
|
+
|
|
147
|
+
when AST::CaseExpr
|
|
148
|
+
generate_case_expr(node)
|
|
149
|
+
|
|
150
|
+
else
|
|
151
|
+
make_literal("/* unknown: #{node.class} */")
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
private
|
|
156
|
+
|
|
157
|
+
# Merge multi-rule function definitions into case expressions
|
|
158
|
+
# e.g., fact(0) = 1; fact(n) = n * fact(n-1);
|
|
159
|
+
# becomes: fact = case { (0) => 1; (n) => n * fact(n-1); }
|
|
160
|
+
def merge_multirule_functions(statements)
|
|
161
|
+
# Group definitions by name
|
|
162
|
+
definition_groups = {}
|
|
163
|
+
other_statements = []
|
|
164
|
+
|
|
165
|
+
statements.each do |stmt|
|
|
166
|
+
if stmt.is_a?(AST::Definition)
|
|
167
|
+
(definition_groups[stmt.name] ||= []) << stmt
|
|
168
|
+
else
|
|
169
|
+
other_statements << stmt
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Process each group
|
|
174
|
+
merged_definitions = []
|
|
175
|
+
definition_groups.each do |name, defs|
|
|
176
|
+
if defs.length == 1
|
|
177
|
+
# Single definition - keep as-is
|
|
178
|
+
merged_definitions << defs[0]
|
|
179
|
+
elsif defs.all? { |d| d.params.length == 1 }
|
|
180
|
+
# Multiple definitions with single param each - merge into case
|
|
181
|
+
# This handles patterns like fact(0) = 1; fact(n) = n * fact(n-1);
|
|
182
|
+
merged_definitions << merge_to_case(name, defs)
|
|
183
|
+
else
|
|
184
|
+
# Multiple definitions but not simple pattern matching
|
|
185
|
+
# Keep the last one (original behavior) with a warning comment
|
|
186
|
+
merged_definitions << defs.last
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Reconstruct statement order: imports, declares, definitions
|
|
191
|
+
other_statements + merged_definitions
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Merge multiple definitions into a single definition with a case expression
|
|
195
|
+
def merge_to_case(name, defs)
|
|
196
|
+
branches = defs.map do |defn|
|
|
197
|
+
# The param becomes the pattern
|
|
198
|
+
param = defn.params[0]
|
|
199
|
+
|
|
200
|
+
# Check if param looks like an integer literal pattern
|
|
201
|
+
# In Faust, fact(0) means param is literally "0"
|
|
202
|
+
pattern = if param =~ /^\d+$/
|
|
203
|
+
AST::IntLiteral.new(param.to_i, line: defn.line, column: defn.column)
|
|
204
|
+
else
|
|
205
|
+
AST::Identifier.new(param, line: defn.line, column: defn.column)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
AST::CaseBranch.new(pattern, defn.expression, line: defn.line, column: defn.column)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
case_expr = AST::CaseExpr.new(branches, line: defs[0].line, column: defs[0].column)
|
|
212
|
+
AST::Definition.new(name, case_expr, params: [], line: defs[0].line, column: defs[0].column)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Check if a node is a numeric literal that needs wrapping for composition
|
|
216
|
+
def numeric_literal?(node)
|
|
217
|
+
node.is_a?(AST::IntLiteral) || node.is_a?(AST::FloatLiteral)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Recursively check if a node is effectively a numeric value
|
|
221
|
+
# (handles parens, negation, etc.)
|
|
222
|
+
def effective_numeric?(node)
|
|
223
|
+
case node
|
|
224
|
+
when AST::IntLiteral, AST::FloatLiteral
|
|
225
|
+
true
|
|
226
|
+
when AST::UnaryOp
|
|
227
|
+
node.op == :NEG && effective_numeric?(node.operand)
|
|
228
|
+
when AST::Paren
|
|
229
|
+
effective_numeric?(node.expression)
|
|
230
|
+
else
|
|
231
|
+
false
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Wrap numeric literals with num() for composition operators
|
|
236
|
+
# Without this, Ruby's >> would be bit-shift instead of DSL sequencing
|
|
237
|
+
def wrap_for_composition(node)
|
|
238
|
+
expr = generate_expression(node)
|
|
239
|
+
if effective_numeric?(node)
|
|
240
|
+
"num(#{expr})"
|
|
241
|
+
else
|
|
242
|
+
expr
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def generate_definition(stmt)
|
|
247
|
+
if stmt.params.empty?
|
|
248
|
+
"#{stmt.name} = #{generate_expression(stmt.expression)}"
|
|
249
|
+
else
|
|
250
|
+
# Downcase parameter names (Ruby doesn't allow constants as formal args)
|
|
251
|
+
params_str = stmt.params.map { |p| ruby_safe_param(p) }.join(", ")
|
|
252
|
+
"def #{stmt.name}(#{params_str})\n #{generate_expression(stmt.expression)}\nend"
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Convert parameter name to Ruby-safe lowercase
|
|
257
|
+
def ruby_safe_param(name)
|
|
258
|
+
# Downcase if it starts with uppercase (would be a constant)
|
|
259
|
+
name[0] =~ /[A-Z]/ ? name.downcase : name
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Generate a literal() call with properly escaped content
|
|
263
|
+
def make_literal(content)
|
|
264
|
+
"literal(#{content.inspect})"
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Extract Faust code from a Ruby expression for embedding in literals
|
|
268
|
+
# e.g., 'literal("foo")' -> 'foo', 'x' -> 'x'
|
|
269
|
+
def to_faust(ruby_expr)
|
|
270
|
+
if ruby_expr =~ /\Aliteral\(["'](.*)["']\)\z/
|
|
271
|
+
$1.gsub(/\\"/, '"') # Unescape quotes
|
|
272
|
+
else
|
|
273
|
+
ruby_expr
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def generate_identifier(node)
|
|
278
|
+
name = node.name
|
|
279
|
+
|
|
280
|
+
# Handle primitive operators used as identifiers
|
|
281
|
+
case name
|
|
282
|
+
when "+"
|
|
283
|
+
"add"
|
|
284
|
+
when "-"
|
|
285
|
+
"sub"
|
|
286
|
+
when "*"
|
|
287
|
+
"mul"
|
|
288
|
+
when "/"
|
|
289
|
+
"div"
|
|
290
|
+
when "mem"
|
|
291
|
+
"mem"
|
|
292
|
+
else
|
|
293
|
+
# Check for known primitives that become method calls
|
|
294
|
+
if LibraryMapper::PRIMITIVES.key?(name)
|
|
295
|
+
mapping = LibraryMapper::PRIMITIVES[name]
|
|
296
|
+
if mapping[:args] == 0
|
|
297
|
+
mapping[:dsl].to_s
|
|
298
|
+
else
|
|
299
|
+
name # Function reference
|
|
300
|
+
end
|
|
301
|
+
elsif @definitions.key?(name)
|
|
302
|
+
# User-defined function used as signal processor
|
|
303
|
+
defn = @definitions[name]
|
|
304
|
+
if defn.params.length == 1
|
|
305
|
+
# Single-param function used point-free: call with wire
|
|
306
|
+
"#{name}(wire)"
|
|
307
|
+
elsif defn.params.empty?
|
|
308
|
+
name # Variable reference
|
|
309
|
+
else
|
|
310
|
+
# Multi-param function - needs partial application
|
|
311
|
+
name
|
|
312
|
+
end
|
|
313
|
+
else
|
|
314
|
+
name
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def generate_qualified_name(node)
|
|
320
|
+
name = node.to_s
|
|
321
|
+
|
|
322
|
+
# Check library mapping
|
|
323
|
+
mapping = LibraryMapper.lookup(name)
|
|
324
|
+
if mapping
|
|
325
|
+
if mapping[:args] == 0
|
|
326
|
+
mapping[:dsl].to_s
|
|
327
|
+
else
|
|
328
|
+
# Return as a method name for partial application
|
|
329
|
+
mapping[:dsl].to_s
|
|
330
|
+
end
|
|
331
|
+
else
|
|
332
|
+
"literal(#{name.inspect})"
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def generate_binary_op(node)
|
|
337
|
+
# For composition operators, wrap numeric literals with num()
|
|
338
|
+
# to avoid Ruby interpreting >> as bit-shift
|
|
339
|
+
case node.op
|
|
340
|
+
when :SEQ, :PAR, :SPLIT, :MERGE, :REC
|
|
341
|
+
left = wrap_for_composition(node.left)
|
|
342
|
+
right = wrap_for_composition(node.right)
|
|
343
|
+
else
|
|
344
|
+
left = generate_expression(node.left)
|
|
345
|
+
right = generate_expression(node.right)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
case node.op
|
|
349
|
+
when :SEQ
|
|
350
|
+
"(#{left} >> #{right})"
|
|
351
|
+
when :PAR
|
|
352
|
+
"(#{left} | #{right})"
|
|
353
|
+
when :SPLIT
|
|
354
|
+
"#{left}.split(#{right})"
|
|
355
|
+
when :MERGE
|
|
356
|
+
"#{left}.merge(#{right})"
|
|
357
|
+
when :REC
|
|
358
|
+
"(#{left} ~ #{right})"
|
|
359
|
+
when :ADD
|
|
360
|
+
"(#{left} + #{right})"
|
|
361
|
+
when :SUB
|
|
362
|
+
"(#{left} - #{right})"
|
|
363
|
+
when :MUL
|
|
364
|
+
"(#{left} * #{right})"
|
|
365
|
+
when :DIV
|
|
366
|
+
"(#{left} / #{right})"
|
|
367
|
+
when :MOD
|
|
368
|
+
"(#{left} % #{right})"
|
|
369
|
+
when :POW
|
|
370
|
+
"pow(#{left}, #{right})"
|
|
371
|
+
when :DELAY
|
|
372
|
+
"delay(#{left}, #{right})"
|
|
373
|
+
when :AND
|
|
374
|
+
"(#{left} & #{right})"
|
|
375
|
+
when :OR
|
|
376
|
+
# Use bor() method to avoid conflict with parallel composition |
|
|
377
|
+
"#{left}.bor(#{right})"
|
|
378
|
+
when :LT
|
|
379
|
+
"(#{left} < #{right})"
|
|
380
|
+
when :GT
|
|
381
|
+
"(#{left} > #{right})"
|
|
382
|
+
when :LE
|
|
383
|
+
"(#{left} <= #{right})"
|
|
384
|
+
when :GE
|
|
385
|
+
"(#{left} >= #{right})"
|
|
386
|
+
when :EQ
|
|
387
|
+
# Use eq() method since Ruby's == must return boolean
|
|
388
|
+
"#{left}.eq(#{right})"
|
|
389
|
+
when :NEQ
|
|
390
|
+
"#{left}.neq(#{right})"
|
|
391
|
+
else
|
|
392
|
+
make_literal("(#{to_faust(left)} #{node.op} #{to_faust(right)})")
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def generate_unary_op(node)
|
|
397
|
+
operand = generate_expression(node.operand)
|
|
398
|
+
|
|
399
|
+
case node.op
|
|
400
|
+
when :NEG
|
|
401
|
+
"(-#{operand})"
|
|
402
|
+
else
|
|
403
|
+
make_literal("#{node.op}(#{to_faust(operand)})")
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def generate_function_call(node)
|
|
408
|
+
name = node.name
|
|
409
|
+
args = node.args.map { |a| generate_expression(a) }
|
|
410
|
+
|
|
411
|
+
# Handle prefix operator forms
|
|
412
|
+
case name
|
|
413
|
+
when "*"
|
|
414
|
+
# *(x) -> gain(x)
|
|
415
|
+
if args.length == 1
|
|
416
|
+
return "gain(#{args[0]})"
|
|
417
|
+
else
|
|
418
|
+
return "(#{args.join(' * ')})"
|
|
419
|
+
end
|
|
420
|
+
when "+"
|
|
421
|
+
# +(x) is a signal processor: input + x
|
|
422
|
+
return args.length == 1 ? "(wire + #{args[0]})" : "(#{args.join(' + ')})"
|
|
423
|
+
when "-"
|
|
424
|
+
# -(x) is a signal processor: input - x
|
|
425
|
+
return args.length == 1 ? "(wire - #{args[0]})" : "(#{args.join(' - ')})"
|
|
426
|
+
when "/"
|
|
427
|
+
return args.length == 1 ? make_literal("/(#{to_faust(args[0])})") : "(#{args.join(' / ')})"
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# Check library mapping
|
|
431
|
+
mapping = LibraryMapper.lookup(name)
|
|
432
|
+
if mapping
|
|
433
|
+
generate_mapped_call(mapping, args, name)
|
|
434
|
+
elsif @definitions.key?(name)
|
|
435
|
+
# User-defined function - call directly as Ruby method
|
|
436
|
+
"#{name}(#{args.join(', ')})"
|
|
437
|
+
else
|
|
438
|
+
# Unknown function - emit as literal with Faust code
|
|
439
|
+
faust_args = args.map { |a| to_faust(a) }.join(", ")
|
|
440
|
+
make_literal("#{name}(#{faust_args})")
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def generate_mapped_call(mapping, args, original_name)
|
|
445
|
+
dsl_method = mapping[:dsl]
|
|
446
|
+
|
|
447
|
+
case dsl_method
|
|
448
|
+
when :lp, :hp
|
|
449
|
+
# fi.lowpass(order, freq) -> lp(freq, order: order)
|
|
450
|
+
if args.length >= 2
|
|
451
|
+
order = args[0]
|
|
452
|
+
freq = args[1]
|
|
453
|
+
"#{dsl_method}(#{freq}, order: #{order})"
|
|
454
|
+
else
|
|
455
|
+
"#{dsl_method}(#{args.join(', ')})"
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
when :slider
|
|
459
|
+
# hslider already parsed as UIElement
|
|
460
|
+
"#{dsl_method}(#{args.join(', ')})"
|
|
461
|
+
|
|
462
|
+
when :selectn
|
|
463
|
+
# ba.selectn(n, idx, ...) -> selectn(n, idx, ...)
|
|
464
|
+
"selectn(#{args.join(', ')})"
|
|
465
|
+
|
|
466
|
+
else
|
|
467
|
+
# Standard call - check for partial application
|
|
468
|
+
expected_args = mapping[:args]
|
|
469
|
+
if expected_args.is_a?(Integer) && args.length < expected_args && args.length > 0
|
|
470
|
+
# Partial application - generate flambda for remaining args
|
|
471
|
+
missing = expected_args - args.length
|
|
472
|
+
if missing == 1
|
|
473
|
+
"flambda(:x) { |x| #{dsl_method}(#{args.join(', ')}, x) }"
|
|
474
|
+
elsif missing == 2
|
|
475
|
+
"flambda(:x, :y) { |x, y| #{dsl_method}(#{args.join(', ')}, x, y) }"
|
|
476
|
+
elsif missing == 3
|
|
477
|
+
"flambda(:x, :y, :z) { |x, y, z| #{dsl_method}(#{args.join(', ')}, x, y, z) }"
|
|
478
|
+
else
|
|
479
|
+
# Too many missing args - use literal
|
|
480
|
+
faust_args = args.map { |a| to_faust(a) }.join(", ")
|
|
481
|
+
make_literal("#{original_name}(#{faust_args})")
|
|
482
|
+
end
|
|
483
|
+
elsif args.empty?
|
|
484
|
+
dsl_method.to_s
|
|
485
|
+
else
|
|
486
|
+
"#{dsl_method}(#{args.join(', ')})"
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def generate_ui_element(node)
|
|
492
|
+
case node.type
|
|
493
|
+
when :hslider
|
|
494
|
+
init = generate_expression(node.init)
|
|
495
|
+
min = generate_expression(node.min)
|
|
496
|
+
max = generate_expression(node.max)
|
|
497
|
+
step = generate_expression(node.step)
|
|
498
|
+
"slider(#{node.label.inspect}, init: #{init}, min: #{min}, max: #{max}, step: #{step})"
|
|
499
|
+
|
|
500
|
+
when :vslider
|
|
501
|
+
init = generate_expression(node.init)
|
|
502
|
+
min = generate_expression(node.min)
|
|
503
|
+
max = generate_expression(node.max)
|
|
504
|
+
step = generate_expression(node.step)
|
|
505
|
+
"vslider(#{node.label.inspect}, init: #{init}, min: #{min}, max: #{max}, step: #{step})"
|
|
506
|
+
|
|
507
|
+
when :nentry
|
|
508
|
+
init = generate_expression(node.init)
|
|
509
|
+
min = generate_expression(node.min)
|
|
510
|
+
max = generate_expression(node.max)
|
|
511
|
+
step = generate_expression(node.step)
|
|
512
|
+
"nentry(#{node.label.inspect}, init: #{init}, min: #{min}, max: #{max}, step: #{step})"
|
|
513
|
+
|
|
514
|
+
when :button
|
|
515
|
+
"button(#{node.label.inspect})"
|
|
516
|
+
|
|
517
|
+
when :checkbox
|
|
518
|
+
"checkbox(#{node.label.inspect})"
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
def generate_ui_group(node)
|
|
523
|
+
content = generate_expression(node.content)
|
|
524
|
+
|
|
525
|
+
case node.type
|
|
526
|
+
when :hgroup
|
|
527
|
+
"hgroup(#{node.label.inspect}) { #{content} }"
|
|
528
|
+
when :vgroup
|
|
529
|
+
"vgroup(#{node.label.inspect}) { #{content} }"
|
|
530
|
+
when :tgroup
|
|
531
|
+
"tgroup(#{node.label.inspect}) { #{content} }"
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def generate_iteration(node)
|
|
536
|
+
var = node.var
|
|
537
|
+
count = generate_expression(node.count)
|
|
538
|
+
body = generate_expression(node.body)
|
|
539
|
+
|
|
540
|
+
method = case node.type
|
|
541
|
+
when :par then "fpar"
|
|
542
|
+
when :seq then "fseq"
|
|
543
|
+
when :sum then "fsum"
|
|
544
|
+
when :prod then "fprod"
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
"#{method}(#{count}) { |#{var}| #{body} }"
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def generate_lambda(node)
|
|
551
|
+
params = node.params.join(", ")
|
|
552
|
+
body = generate_expression(node.body)
|
|
553
|
+
|
|
554
|
+
if node.params.length == 1
|
|
555
|
+
"flambda(:#{node.params[0]}) { |#{params}| #{body} }"
|
|
556
|
+
else
|
|
557
|
+
params_syms = node.params.map { |p| ":#{p}" }.join(", ")
|
|
558
|
+
"flambda(#{params_syms}) { |#{params}| #{body} }"
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def generate_waveform(node)
|
|
563
|
+
values = node.values.map { |v| generate_expression(v) }
|
|
564
|
+
"waveform(#{values.join(', ')})"
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def generate_table(node)
|
|
568
|
+
args = node.args.map { |a| generate_expression(a) }
|
|
569
|
+
|
|
570
|
+
case node.type
|
|
571
|
+
when :rdtable
|
|
572
|
+
"rdtable(#{args.join(', ')})"
|
|
573
|
+
when :rwtable
|
|
574
|
+
"rwtable(#{args.join(', ')})"
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def generate_route(node)
|
|
579
|
+
ins = generate_expression(node.ins)
|
|
580
|
+
outs = generate_expression(node.outs)
|
|
581
|
+
connections = node.connections.map do |from, to|
|
|
582
|
+
"[#{generate_expression(from)}, #{generate_expression(to)}]"
|
|
583
|
+
end
|
|
584
|
+
"route(#{ins}, #{outs}, [#{connections.join(', ')}])"
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def generate_prime(node)
|
|
588
|
+
operand = generate_expression(node.operand)
|
|
589
|
+
"(#{operand} >> mem)"
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
def generate_access(node)
|
|
593
|
+
operand = generate_expression(node.operand)
|
|
594
|
+
index = generate_expression(node.index)
|
|
595
|
+
make_literal("#{to_faust(operand)}[#{to_faust(index)}]")
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
def generate_with(node)
|
|
599
|
+
# Generate local definitions inside a lambda for proper scoping
|
|
600
|
+
lines = ["-> {"]
|
|
601
|
+
|
|
602
|
+
# Add local definitions to @definitions for forward reference support
|
|
603
|
+
local_defs = {}
|
|
604
|
+
node.definitions.each do |defn|
|
|
605
|
+
local_defs[defn.name] = defn
|
|
606
|
+
@definitions[defn.name] = defn
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
# Generate each local definition
|
|
610
|
+
node.definitions.each do |defn|
|
|
611
|
+
if defn.params.empty?
|
|
612
|
+
# Simple variable definition
|
|
613
|
+
lines << " #{defn.name} = #{generate_expression(defn.expression)}"
|
|
614
|
+
else
|
|
615
|
+
# Function definition - generate as flambda (creates DSP node)
|
|
616
|
+
params = defn.params.map { |p| ruby_safe_param(p) }
|
|
617
|
+
params_syms = params.map { |p| ":#{p}" }.join(", ")
|
|
618
|
+
params_str = params.join(", ")
|
|
619
|
+
lines << " #{defn.name} = flambda(#{params_syms}) { |#{params_str}| #{generate_expression(defn.expression)} }"
|
|
620
|
+
end
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
# Generate the main expression
|
|
624
|
+
lines << " #{generate_expression(node.expression)}"
|
|
625
|
+
lines << "}.call"
|
|
626
|
+
|
|
627
|
+
# Remove local definitions from @definitions (restore scope)
|
|
628
|
+
local_defs.each_key { |name| @definitions.delete(name) }
|
|
629
|
+
|
|
630
|
+
lines.join("\n")
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def generate_letrec(node)
|
|
634
|
+
# Letrec is complex - generate as literal for now
|
|
635
|
+
defs = node.definitions.map do |d|
|
|
636
|
+
"#{d.name} = #{to_faust(generate_expression(d.expression))}"
|
|
637
|
+
end.join("; ")
|
|
638
|
+
expr = node.expression ? to_faust(generate_expression(node.expression)) : "_"
|
|
639
|
+
make_literal("letrec { #{defs} } #{expr}")
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
# Generate code for case expressions
|
|
643
|
+
# case { (0) => a; (1) => b; (n) => c; }
|
|
644
|
+
# converts to: flambda(:x) { |x| select2(x.eq(0), select2(x.eq(1), c, b), a) }
|
|
645
|
+
def generate_case_expr(node)
|
|
646
|
+
branches = node.branches
|
|
647
|
+
|
|
648
|
+
# Separate integer patterns from variable patterns (catch-all)
|
|
649
|
+
int_branches = []
|
|
650
|
+
default_branch = nil
|
|
651
|
+
|
|
652
|
+
branches.each do |branch|
|
|
653
|
+
pattern = branch.pattern
|
|
654
|
+
if pattern.is_a?(AST::IntLiteral) || (pattern.is_a?(AST::Paren) && pattern.expression.is_a?(AST::IntLiteral))
|
|
655
|
+
# Integer pattern
|
|
656
|
+
val = pattern.is_a?(AST::Paren) ? pattern.expression.value : pattern.value
|
|
657
|
+
int_branches << { value: val, result: branch.result }
|
|
658
|
+
elsif pattern.is_a?(AST::Identifier) || (pattern.is_a?(AST::Paren) && pattern.expression.is_a?(AST::Identifier))
|
|
659
|
+
# Variable pattern - this is the default/catch-all case
|
|
660
|
+
var_name = pattern.is_a?(AST::Paren) ? pattern.expression.name : pattern.name
|
|
661
|
+
default_branch = { var: var_name, result: branch.result }
|
|
662
|
+
else
|
|
663
|
+
# Complex pattern - fall back to literal
|
|
664
|
+
return generate_case_literal(node)
|
|
665
|
+
end
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
# If no integer patterns at all, fall back to literal
|
|
669
|
+
if int_branches.empty?
|
|
670
|
+
return generate_case_literal(node)
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
# Generate a flambda that takes the input and uses select2 chains
|
|
674
|
+
# We need to use the variable name from default branch if present, else use 'x'
|
|
675
|
+
var = default_branch ? default_branch[:var] : "x"
|
|
676
|
+
var = ruby_safe_param(var)
|
|
677
|
+
|
|
678
|
+
# Build the select2 chain from inside out
|
|
679
|
+
# Start with the default/else case (or the last integer branch result if no default)
|
|
680
|
+
if default_branch
|
|
681
|
+
inner = generate_expression(default_branch[:result])
|
|
682
|
+
else
|
|
683
|
+
# No default - use the last branch result as fallback (arbitrary choice)
|
|
684
|
+
inner = generate_expression(int_branches.last[:result])
|
|
685
|
+
int_branches = int_branches[0...-1]
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
# Wrap each integer pattern with select2
|
|
689
|
+
# select2(cond, false_val, true_val) - condition true returns second arg
|
|
690
|
+
int_branches.reverse_each do |branch|
|
|
691
|
+
result = generate_expression(branch[:result])
|
|
692
|
+
inner = "select2(#{var}.eq(#{branch[:value]}), #{inner}, #{result})"
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
"flambda(:#{var}) { |#{var}| #{inner} }"
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
# Fall back to literal for complex case expressions
|
|
699
|
+
def generate_case_literal(node)
|
|
700
|
+
branches_str = node.branches.map do |branch|
|
|
701
|
+
pattern_str = to_faust(generate_expression(branch.pattern))
|
|
702
|
+
result_str = to_faust(generate_expression(branch.result))
|
|
703
|
+
"(#{pattern_str}) => #{result_str}"
|
|
704
|
+
end.join("; ")
|
|
705
|
+
make_literal("case { #{branches_str}; }")
|
|
706
|
+
end
|
|
707
|
+
end
|
|
708
|
+
end
|