cyclotone 0.1.0 → 1.0.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 +4 -4
- data/README.md +8 -74
- data/Rakefile +37 -1
- data/cyclotone.gemspec +16 -3
- data/exe/cyclotone +12 -0
- data/lib/cyclotone/backends/midi_backend.rb +106 -21
- data/lib/cyclotone/backends/midi_file_backend.rb +182 -24
- data/lib/cyclotone/backends/midi_message_support.rb +111 -28
- data/lib/cyclotone/backends/null_backend.rb +33 -0
- data/lib/cyclotone/backends/osc_backend.rb +105 -17
- data/lib/cyclotone/controls.rb +64 -16
- data/lib/cyclotone/dsl.rb +5 -5
- data/lib/cyclotone/errors.rb +8 -3
- data/lib/cyclotone/event.rb +38 -3
- data/lib/cyclotone/harmony.rb +62 -8
- data/lib/cyclotone/mini_notation/ast.rb +85 -5
- data/lib/cyclotone/mini_notation/compiler.rb +18 -10
- data/lib/cyclotone/mini_notation/parser.rb +168 -34
- data/lib/cyclotone/oscillators.rb +130 -28
- data/lib/cyclotone/pattern.rb +211 -36
- data/lib/cyclotone/scheduler.rb +179 -40
- data/lib/cyclotone/state.rb +0 -1
- data/lib/cyclotone/stream.rb +91 -45
- data/lib/cyclotone/support/deterministic.rb +37 -1
- data/lib/cyclotone/time_span.rb +29 -7
- data/lib/cyclotone/transforms/accumulation.rb +28 -5
- data/lib/cyclotone/transforms/alteration.rb +82 -18
- data/lib/cyclotone/transforms/condition.rb +15 -3
- data/lib/cyclotone/transforms/sample.rb +33 -9
- data/lib/cyclotone/transforms/time.rb +24 -5
- data/lib/cyclotone/transition.rb +54 -42
- data/lib/cyclotone/version.rb +1 -1
- data/lib/cyclotone.rb +1 -0
- data/sig/cyclotone.rbs +99 -0
- metadata +4 -1
|
@@ -4,6 +4,19 @@ module Cyclotone
|
|
|
4
4
|
module MiniNotation
|
|
5
5
|
module AST
|
|
6
6
|
class Node
|
|
7
|
+
def self.deep_freeze(value)
|
|
8
|
+
case value
|
|
9
|
+
when Array
|
|
10
|
+
value.map { |entry| deep_freeze(entry) }.freeze
|
|
11
|
+
when Hash
|
|
12
|
+
value.each_with_object({}) do |(key, entry), frozen_hash|
|
|
13
|
+
frozen_hash[deep_freeze(key)] = deep_freeze(entry)
|
|
14
|
+
end.freeze
|
|
15
|
+
else
|
|
16
|
+
value.freeze
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
7
20
|
def ==(other)
|
|
8
21
|
other.is_a?(self.class) && to_h == other.to_h
|
|
9
22
|
end
|
|
@@ -13,12 +26,17 @@ module Cyclotone
|
|
|
13
26
|
def hash
|
|
14
27
|
[self.class, to_h].hash
|
|
15
28
|
end
|
|
29
|
+
|
|
30
|
+
def to_mn
|
|
31
|
+
AST.to_source(self)
|
|
32
|
+
end
|
|
16
33
|
end
|
|
17
34
|
|
|
18
35
|
class Atom < Node
|
|
19
36
|
attr_reader :value, :sample
|
|
20
37
|
|
|
21
38
|
def initialize(value:, sample: nil)
|
|
39
|
+
super()
|
|
22
40
|
@value = value
|
|
23
41
|
@sample = sample
|
|
24
42
|
freeze
|
|
@@ -43,7 +61,8 @@ module Cyclotone
|
|
|
43
61
|
attr_reader :elements
|
|
44
62
|
|
|
45
63
|
def initialize(elements:)
|
|
46
|
-
|
|
64
|
+
super()
|
|
65
|
+
@elements = Node.deep_freeze(elements)
|
|
47
66
|
freeze
|
|
48
67
|
end
|
|
49
68
|
|
|
@@ -56,7 +75,8 @@ module Cyclotone
|
|
|
56
75
|
attr_reader :patterns
|
|
57
76
|
|
|
58
77
|
def initialize(patterns:)
|
|
59
|
-
|
|
78
|
+
super()
|
|
79
|
+
@patterns = Node.deep_freeze(patterns)
|
|
60
80
|
freeze
|
|
61
81
|
end
|
|
62
82
|
|
|
@@ -69,7 +89,8 @@ module Cyclotone
|
|
|
69
89
|
attr_reader :patterns
|
|
70
90
|
|
|
71
91
|
def initialize(patterns:)
|
|
72
|
-
|
|
92
|
+
super()
|
|
93
|
+
@patterns = Node.deep_freeze(patterns)
|
|
73
94
|
freeze
|
|
74
95
|
end
|
|
75
96
|
|
|
@@ -82,6 +103,7 @@ module Cyclotone
|
|
|
82
103
|
attr_reader :pattern, :count
|
|
83
104
|
|
|
84
105
|
def initialize(pattern:, count:)
|
|
106
|
+
super()
|
|
85
107
|
@pattern = pattern
|
|
86
108
|
@count = count.to_i
|
|
87
109
|
freeze
|
|
@@ -96,6 +118,7 @@ module Cyclotone
|
|
|
96
118
|
attr_reader :pattern, :count
|
|
97
119
|
|
|
98
120
|
def initialize(pattern:, count:)
|
|
121
|
+
super()
|
|
99
122
|
@pattern = pattern
|
|
100
123
|
@count = count.to_i
|
|
101
124
|
freeze
|
|
@@ -110,6 +133,7 @@ module Cyclotone
|
|
|
110
133
|
attr_reader :pattern, :amount
|
|
111
134
|
|
|
112
135
|
def initialize(pattern:, amount:)
|
|
136
|
+
super()
|
|
113
137
|
@pattern = pattern
|
|
114
138
|
@amount = amount
|
|
115
139
|
freeze
|
|
@@ -124,6 +148,7 @@ module Cyclotone
|
|
|
124
148
|
attr_reader :pattern, :amount
|
|
125
149
|
|
|
126
150
|
def initialize(pattern:, amount:)
|
|
151
|
+
super()
|
|
127
152
|
@pattern = pattern
|
|
128
153
|
@amount = amount
|
|
129
154
|
freeze
|
|
@@ -142,6 +167,7 @@ module Cyclotone
|
|
|
142
167
|
attr_reader :pattern, :probability
|
|
143
168
|
|
|
144
169
|
def initialize(pattern:, probability:)
|
|
170
|
+
super()
|
|
145
171
|
@pattern = pattern
|
|
146
172
|
@probability = probability
|
|
147
173
|
freeze
|
|
@@ -156,7 +182,8 @@ module Cyclotone
|
|
|
156
182
|
attr_reader :patterns
|
|
157
183
|
|
|
158
184
|
def initialize(patterns:)
|
|
159
|
-
|
|
185
|
+
super()
|
|
186
|
+
@patterns = Node.deep_freeze(patterns)
|
|
160
187
|
freeze
|
|
161
188
|
end
|
|
162
189
|
|
|
@@ -169,6 +196,7 @@ module Cyclotone
|
|
|
169
196
|
attr_reader :pattern, :pulses, :steps, :rotation
|
|
170
197
|
|
|
171
198
|
def initialize(pattern:, pulses:, steps:, rotation: 0)
|
|
199
|
+
super()
|
|
172
200
|
@pattern = pattern
|
|
173
201
|
@pulses = pulses.to_i
|
|
174
202
|
@steps = steps.to_i
|
|
@@ -185,7 +213,8 @@ module Cyclotone
|
|
|
185
213
|
attr_reader :patterns, :steps
|
|
186
214
|
|
|
187
215
|
def initialize(patterns:, steps: nil)
|
|
188
|
-
|
|
216
|
+
super()
|
|
217
|
+
@patterns = Node.deep_freeze(patterns)
|
|
189
218
|
@steps = steps&.to_i
|
|
190
219
|
freeze
|
|
191
220
|
end
|
|
@@ -194,6 +223,57 @@ module Cyclotone
|
|
|
194
223
|
{ patterns: patterns, steps: steps }
|
|
195
224
|
end
|
|
196
225
|
end
|
|
226
|
+
|
|
227
|
+
module_function
|
|
228
|
+
|
|
229
|
+
def to_source(node)
|
|
230
|
+
case node
|
|
231
|
+
when Atom
|
|
232
|
+
node.sample ? "#{format_atom(node.value)}:#{node.sample}" : format_atom(node.value)
|
|
233
|
+
when Rest
|
|
234
|
+
"~"
|
|
235
|
+
when Sequence
|
|
236
|
+
node.elements.map { |element| to_source(element) }.join(" ")
|
|
237
|
+
when Stack
|
|
238
|
+
"[#{node.patterns.map { |pattern| to_source(pattern) }.join(", ")}]"
|
|
239
|
+
when Alternating
|
|
240
|
+
"<#{node.patterns.map { |pattern| to_source(pattern) }.join(" ")}>"
|
|
241
|
+
when Repeat
|
|
242
|
+
"#{group_if_needed(node.pattern)}*#{node.count}"
|
|
243
|
+
when Replicate
|
|
244
|
+
"#{group_if_needed(node.pattern)}!#{node.count}"
|
|
245
|
+
when Slow
|
|
246
|
+
"#{group_if_needed(node.pattern)}/#{node.amount}"
|
|
247
|
+
when Elongate
|
|
248
|
+
"#{group_if_needed(node.pattern)}@#{node.amount}"
|
|
249
|
+
when Degrade
|
|
250
|
+
"#{group_if_needed(node.pattern)}?#{node.probability}"
|
|
251
|
+
when Choice
|
|
252
|
+
node.patterns.map { |pattern| to_source(pattern) }.join(" | ")
|
|
253
|
+
when Euclidean
|
|
254
|
+
"#{group_if_needed(node.pattern)}(#{node.pulses},#{node.steps},#{node.rotation})"
|
|
255
|
+
when Polymetric
|
|
256
|
+
suffix = node.steps ? "%#{node.steps}" : ""
|
|
257
|
+
"{#{node.patterns.map { |pattern| to_source(pattern) }.join(", ")}}#{suffix}"
|
|
258
|
+
else
|
|
259
|
+
raise ArgumentError, "unsupported AST node #{node.class}"
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def group_if_needed(node)
|
|
264
|
+
node.is_a?(Atom) || node.is_a?(Rest) ? to_source(node) : "[#{to_source(node)}]"
|
|
265
|
+
end
|
|
266
|
+
private_class_method :group_if_needed
|
|
267
|
+
|
|
268
|
+
def format_atom(value)
|
|
269
|
+
return "#{value.numerator}/#{value.denominator}" if value.is_a?(Rational)
|
|
270
|
+
|
|
271
|
+
text = value.to_s
|
|
272
|
+
return text if text.match?(/\A[\w.-]+\z/)
|
|
273
|
+
|
|
274
|
+
"\"#{text.gsub("\\", "\\\\\\").gsub("\"", "\\\"")}\""
|
|
275
|
+
end
|
|
276
|
+
private_class_method :format_atom
|
|
197
277
|
end
|
|
198
278
|
end
|
|
199
279
|
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module Cyclotone
|
|
4
4
|
module MiniNotation
|
|
5
5
|
class Compiler
|
|
6
|
+
MAX_EXPANSION = 4096
|
|
7
|
+
|
|
6
8
|
def compile(node)
|
|
7
9
|
case node
|
|
8
10
|
when AST::Atom
|
|
@@ -16,12 +18,12 @@ module Cyclotone
|
|
|
16
18
|
when AST::Alternating
|
|
17
19
|
compile_alternating(node)
|
|
18
20
|
when AST::Repeat
|
|
21
|
+
validate_expansion_count(node.count, "repeat count")
|
|
19
22
|
compile(node.pattern).fast(node.count)
|
|
20
23
|
when AST::Replicate
|
|
24
|
+
validate_expansion_count(node.count, "replicate count")
|
|
21
25
|
Pattern.timecat(Array.new(node.count) { [1, compile(node.pattern)] })
|
|
22
|
-
when AST::Slow
|
|
23
|
-
compile(node.pattern).slow(node.amount)
|
|
24
|
-
when AST::Elongate
|
|
26
|
+
when AST::Slow, AST::Elongate
|
|
25
27
|
compile(node.pattern).slow(node.amount)
|
|
26
28
|
when AST::Degrade
|
|
27
29
|
compile(node.pattern).degrade_by(node.probability)
|
|
@@ -60,6 +62,7 @@ module Cyclotone
|
|
|
60
62
|
|
|
61
63
|
def compile_alternating(node)
|
|
62
64
|
patterns = node.patterns.map { |pattern| compile(pattern) }
|
|
65
|
+
raise ArgumentError, "alternating requires patterns" if patterns.empty?
|
|
63
66
|
|
|
64
67
|
Pattern.new do |span|
|
|
65
68
|
cycle = span.cycle_number
|
|
@@ -77,21 +80,21 @@ module Cyclotone
|
|
|
77
80
|
end
|
|
78
81
|
|
|
79
82
|
def compile_polymetric(node)
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
raise ArgumentError, "polymetric requires patterns" if node.patterns.empty?
|
|
84
|
+
|
|
85
|
+
base_steps = Pattern.to_rational(node.steps || step_count(node.patterns.first))
|
|
86
|
+
raise ArgumentError, "polymetric steps must be positive" unless base_steps.positive?
|
|
82
87
|
|
|
83
88
|
Pattern.stack(
|
|
84
89
|
node.patterns.map do |pattern|
|
|
85
|
-
pattern_steps = [step_count(pattern), 1].max
|
|
86
|
-
compile(pattern).slow(
|
|
90
|
+
pattern_steps = [Pattern.to_rational(step_count(pattern)), Rational(1)].max
|
|
91
|
+
compile(pattern).slow(pattern_steps / base_steps)
|
|
87
92
|
end
|
|
88
93
|
)
|
|
89
94
|
end
|
|
90
95
|
|
|
91
96
|
def step_count(node)
|
|
92
97
|
case node
|
|
93
|
-
when AST::Atom, AST::Rest
|
|
94
|
-
1
|
|
95
98
|
when AST::Sequence
|
|
96
99
|
node.elements.sum { |element| step_count(element) }
|
|
97
100
|
when AST::Stack, AST::Alternating, AST::Choice
|
|
@@ -101,7 +104,7 @@ module Cyclotone
|
|
|
101
104
|
when AST::Slow, AST::Degrade
|
|
102
105
|
step_count(node.pattern)
|
|
103
106
|
when AST::Elongate
|
|
104
|
-
step_count(node.pattern) * node.amount
|
|
107
|
+
Pattern.to_rational(step_count(node.pattern)) * Pattern.to_rational(node.amount)
|
|
105
108
|
when AST::Euclidean
|
|
106
109
|
[node.steps, 1].max * step_count(node.pattern)
|
|
107
110
|
when AST::Polymetric
|
|
@@ -110,6 +113,11 @@ module Cyclotone
|
|
|
110
113
|
1
|
|
111
114
|
end
|
|
112
115
|
end
|
|
116
|
+
|
|
117
|
+
def validate_expansion_count(count, label)
|
|
118
|
+
raise ArgumentError, "#{label} must be positive" unless count.positive?
|
|
119
|
+
raise ArgumentError, "#{label} must be <= #{MAX_EXPANSION}" if count > MAX_EXPANSION
|
|
120
|
+
end
|
|
113
121
|
end
|
|
114
122
|
end
|
|
115
123
|
end
|
|
@@ -27,11 +27,12 @@ module Cyclotone
|
|
|
27
27
|
}.freeze
|
|
28
28
|
|
|
29
29
|
def parse(input)
|
|
30
|
-
@
|
|
30
|
+
@source = input.to_s
|
|
31
|
+
@tokens = tokenize(@source)
|
|
31
32
|
@index = 0
|
|
32
33
|
|
|
33
34
|
skip_spaces
|
|
34
|
-
raise ParseError.new("input is empty", line: 1, column: 1) if current.type == :eof
|
|
35
|
+
raise ParseError.new("input is empty", line: 1, column: 1, source: @source) if current.type == :eof
|
|
35
36
|
|
|
36
37
|
ast = parse_stack(terminators: [:eof])
|
|
37
38
|
skip_spaces
|
|
@@ -86,6 +87,12 @@ module Cyclotone
|
|
|
86
87
|
next
|
|
87
88
|
end
|
|
88
89
|
|
|
90
|
+
if char.match?(/[0-9]/) || (char == "-" && input[index + 1]&.match?(/[0-9]/))
|
|
91
|
+
token, index, column = tokenize_number(input, index, line, column)
|
|
92
|
+
tokens << token
|
|
93
|
+
next
|
|
94
|
+
end
|
|
95
|
+
|
|
89
96
|
if SINGLE_CHAR_TOKENS.key?(char)
|
|
90
97
|
tokens << Token.new(type: SINGLE_CHAR_TOKENS[char], value: char, line: line, column: column)
|
|
91
98
|
index += 1
|
|
@@ -93,15 +100,9 @@ module Cyclotone
|
|
|
93
100
|
next
|
|
94
101
|
end
|
|
95
102
|
|
|
96
|
-
if
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
while index < input.length && input[index].match?(/[0-9.]/)
|
|
100
|
-
index += 1
|
|
101
|
-
column += 1
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
tokens << Token.new(type: :number, value: input[start_index...index], line: line, column: start_column)
|
|
103
|
+
if %w[" '].include?(char)
|
|
104
|
+
token, index, column = tokenize_quoted(input, index, line, column)
|
|
105
|
+
tokens << token
|
|
105
106
|
next
|
|
106
107
|
end
|
|
107
108
|
|
|
@@ -130,6 +131,8 @@ module Cyclotone
|
|
|
130
131
|
|
|
131
132
|
while accept(:comma)
|
|
132
133
|
skip_spaces
|
|
134
|
+
raise parse_error("empty stack branch") if terminators.include?(current.type) || current.type == :comma
|
|
135
|
+
|
|
133
136
|
patterns << parse_choice(terminators: terminators + [:comma])
|
|
134
137
|
skip_spaces
|
|
135
138
|
end
|
|
@@ -145,6 +148,8 @@ module Cyclotone
|
|
|
145
148
|
|
|
146
149
|
while accept(:pipe)
|
|
147
150
|
skip_spaces
|
|
151
|
+
raise parse_error("empty choice branch") if terminators.include?(current.type) || current.type == :pipe
|
|
152
|
+
|
|
148
153
|
patterns << parse_sequence(terminators: terminators + [:pipe])
|
|
149
154
|
skip_spaces
|
|
150
155
|
end
|
|
@@ -199,26 +204,30 @@ module Cyclotone
|
|
|
199
204
|
node = case current.type
|
|
200
205
|
when :star
|
|
201
206
|
advance
|
|
202
|
-
AST::Repeat.new(pattern: node, count:
|
|
207
|
+
AST::Repeat.new(pattern: node, count: parse_positive_integer("repeat count"))
|
|
203
208
|
when :bang
|
|
204
209
|
advance
|
|
205
|
-
AST::Replicate.new(pattern: node, count:
|
|
210
|
+
AST::Replicate.new(pattern: node, count: parse_positive_integer("replicate count"))
|
|
206
211
|
when :slash
|
|
207
212
|
advance
|
|
208
|
-
AST::Slow.new(pattern: node, amount:
|
|
213
|
+
AST::Slow.new(pattern: node, amount: parse_positive_number("slow amount"))
|
|
209
214
|
when :at
|
|
210
215
|
advance
|
|
211
|
-
AST::Elongate.new(pattern: node, amount:
|
|
216
|
+
AST::Elongate.new(pattern: node, amount: parse_positive_number("elongate amount"))
|
|
212
217
|
when :question
|
|
213
218
|
advance
|
|
214
|
-
probability = current.type == :number
|
|
219
|
+
probability = if current.type == :number
|
|
220
|
+
parse_probability
|
|
221
|
+
elsif implicit_probability_token?(current.type)
|
|
222
|
+
0.5
|
|
223
|
+
else
|
|
224
|
+
raise parse_error("probability must be between 0 and 1")
|
|
225
|
+
end
|
|
215
226
|
AST::Degrade.new(pattern: node, probability: probability)
|
|
216
227
|
when :colon
|
|
217
228
|
advance
|
|
218
|
-
sample =
|
|
219
|
-
unless node.is_a?(AST::Atom)
|
|
220
|
-
raise parse_error("sample suffix can only be applied to atoms")
|
|
221
|
-
end
|
|
229
|
+
sample = parse_non_negative_integer("sample number")
|
|
230
|
+
raise parse_error("sample suffix can only be applied to atoms") unless node.is_a?(AST::Atom)
|
|
222
231
|
|
|
223
232
|
node.with_sample(sample)
|
|
224
233
|
when :lparen
|
|
@@ -240,8 +249,7 @@ module Cyclotone
|
|
|
240
249
|
AST::Atom.new(value: token.value)
|
|
241
250
|
when :number
|
|
242
251
|
advance
|
|
243
|
-
|
|
244
|
-
AST::Atom.new(value: numeric_value)
|
|
252
|
+
AST::Atom.new(value: number_value(token.value))
|
|
245
253
|
when :tilde
|
|
246
254
|
advance
|
|
247
255
|
AST::Rest.new
|
|
@@ -264,14 +272,12 @@ module Cyclotone
|
|
|
264
272
|
|
|
265
273
|
def parse_euclidean(node)
|
|
266
274
|
expect(:lparen)
|
|
267
|
-
pulses =
|
|
275
|
+
pulses = parse_non_negative_integer("euclidean pulses")
|
|
268
276
|
expect(:comma)
|
|
269
|
-
steps =
|
|
277
|
+
steps = parse_positive_integer("euclidean steps")
|
|
270
278
|
rotation = 0
|
|
271
279
|
|
|
272
|
-
if accept(:comma)
|
|
273
|
-
rotation = parse_integer
|
|
274
|
-
end
|
|
280
|
+
rotation = parse_integer("euclidean rotation") if accept(:comma)
|
|
275
281
|
|
|
276
282
|
expect(:rparen)
|
|
277
283
|
AST::Euclidean.new(pattern: node, pulses: pulses, steps: steps, rotation: rotation)
|
|
@@ -279,14 +285,18 @@ module Cyclotone
|
|
|
279
285
|
|
|
280
286
|
def parse_polymetric
|
|
281
287
|
expect(:lbrace)
|
|
282
|
-
|
|
288
|
+
raise parse_error("empty polymetric branch") if current.type == :rbrace
|
|
289
|
+
|
|
290
|
+
patterns = [parse_sequence(terminators: %i[comma rbrace])]
|
|
283
291
|
|
|
284
292
|
while accept(:comma)
|
|
285
|
-
|
|
293
|
+
raise parse_error("empty polymetric branch") if %i[comma rbrace].include?(current.type)
|
|
294
|
+
|
|
295
|
+
patterns << parse_sequence(terminators: %i[comma rbrace])
|
|
286
296
|
end
|
|
287
297
|
|
|
288
298
|
expect(:rbrace)
|
|
289
|
-
steps = accept(:percent) ?
|
|
299
|
+
steps = accept(:percent) ? parse_positive_integer("polymetric steps") : nil
|
|
290
300
|
|
|
291
301
|
AST::Polymetric.new(patterns: patterns, steps: steps)
|
|
292
302
|
end
|
|
@@ -294,11 +304,42 @@ module Cyclotone
|
|
|
294
304
|
def parse_number
|
|
295
305
|
token = expect(:number)
|
|
296
306
|
|
|
297
|
-
token.value
|
|
307
|
+
number_value(token.value)
|
|
298
308
|
end
|
|
299
309
|
|
|
300
|
-
def parse_integer
|
|
301
|
-
expect(:number)
|
|
310
|
+
def parse_integer(label = "integer")
|
|
311
|
+
token = expect(:number)
|
|
312
|
+
raise parse_error("#{label} must be an integer") if token.value.include?(".") || token.value.include?("/")
|
|
313
|
+
|
|
314
|
+
token.value.to_i
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def parse_positive_number(label)
|
|
318
|
+
value = parse_number
|
|
319
|
+
raise parse_error("#{label} must be positive") unless value.positive?
|
|
320
|
+
|
|
321
|
+
value
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def parse_probability
|
|
325
|
+
value = parse_number
|
|
326
|
+
raise parse_error("probability must be between 0 and 1") unless value.between?(0, 1)
|
|
327
|
+
|
|
328
|
+
value
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def parse_positive_integer(label)
|
|
332
|
+
value = parse_integer(label)
|
|
333
|
+
raise parse_error("#{label} must be positive") unless value.positive?
|
|
334
|
+
|
|
335
|
+
value
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def parse_non_negative_integer(label)
|
|
339
|
+
value = parse_integer(label)
|
|
340
|
+
raise parse_error("#{label} must be non-negative") if value.negative?
|
|
341
|
+
|
|
342
|
+
value
|
|
302
343
|
end
|
|
303
344
|
|
|
304
345
|
def build_group(elements)
|
|
@@ -343,7 +384,100 @@ module Cyclotone
|
|
|
343
384
|
end
|
|
344
385
|
|
|
345
386
|
def parse_error(message)
|
|
346
|
-
ParseError.new(message, line: current.line, column: current.column)
|
|
387
|
+
ParseError.new(message, line: current.line, column: current.column, source: @source)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def implicit_probability_token?(type)
|
|
391
|
+
%i[
|
|
392
|
+
space eof rbracket rangle rbrace comma pipe dot star bang slash at question colon lparen
|
|
393
|
+
].include?(type)
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def tokenize_number(input, index, line, column)
|
|
397
|
+
start_index = index
|
|
398
|
+
start_column = column
|
|
399
|
+
|
|
400
|
+
if input[index] == "-"
|
|
401
|
+
index += 1
|
|
402
|
+
column += 1
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
while index < input.length && input[index].match?(/[0-9]/)
|
|
406
|
+
index += 1
|
|
407
|
+
column += 1
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
if input[index] == "."
|
|
411
|
+
unless input[index + 1]&.match?(/[0-9]/)
|
|
412
|
+
raise ParseError.new("invalid number literal", line: line, column: column, source: @source)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
index += 1
|
|
416
|
+
column += 1
|
|
417
|
+
|
|
418
|
+
while index < input.length && input[index].match?(/[0-9]/)
|
|
419
|
+
index += 1
|
|
420
|
+
column += 1
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
raise ParseError.new("invalid number literal", line: line, column: column, source: @source) if input[index] == "."
|
|
425
|
+
|
|
426
|
+
if input[index] == "/" && input[index + 1]&.match?(/[0-9]/)
|
|
427
|
+
index += 1
|
|
428
|
+
column += 1
|
|
429
|
+
denominator_start = index
|
|
430
|
+
|
|
431
|
+
while index < input.length && input[index].match?(/[0-9]/)
|
|
432
|
+
index += 1
|
|
433
|
+
column += 1
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
if input[denominator_start...index].to_i.zero?
|
|
437
|
+
raise ParseError.new("rational denominator must be positive", line: line, column: denominator_start + 1, source: @source)
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
[Token.new(type: :number, value: input[start_index...index], line: line, column: start_column), index, column]
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def number_value(value)
|
|
445
|
+
return Rational(value) if value.include?("/")
|
|
446
|
+
return value.to_f if value.include?(".")
|
|
447
|
+
|
|
448
|
+
value.to_i
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def tokenize_quoted(input, index, line, column)
|
|
452
|
+
quote = input[index]
|
|
453
|
+
start_column = column
|
|
454
|
+
index += 1
|
|
455
|
+
column += 1
|
|
456
|
+
value = +""
|
|
457
|
+
|
|
458
|
+
while index < input.length
|
|
459
|
+
char = input[index]
|
|
460
|
+
|
|
461
|
+
if char == quote
|
|
462
|
+
index += 1
|
|
463
|
+
column += 1
|
|
464
|
+
return [Token.new(type: :word, value: value, line: line, column: start_column), index, column]
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
if char == "\\"
|
|
468
|
+
index += 1
|
|
469
|
+
column += 1
|
|
470
|
+
raise ParseError.new("unterminated escape sequence", line: line, column: column, source: @source) if index >= input.length
|
|
471
|
+
|
|
472
|
+
char = input[index]
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
value << char
|
|
476
|
+
index += 1
|
|
477
|
+
column += 1
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
raise ParseError.new("unterminated quoted atom", line: line, column: start_column, source: @source)
|
|
347
481
|
end
|
|
348
482
|
end
|
|
349
483
|
end
|