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.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Faust2Ruby
4
+ VERSION = "0.1.0"
5
+ end