srsh 0.7.1 → 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/LICENSE +18 -14
- data/README.md +351 -134
- data/bin/srsh +53 -1473
- data/docs/assets/slut.txt +4 -0
- data/docs/assets/srsh-mark.svg +12 -0
- data/docs/css/style.css +696 -0
- data/docs/index.html +703 -0
- data/docs/js/app.js +203 -0
- data/examples/bridge.rsh +8 -0
- data/examples/calculator.rsh +253 -0
- data/examples/defer.rsh +14 -0
- data/examples/hot.rsh +14 -0
- data/examples/meta.rsh +20 -0
- data/examples/modules/text.rsh +6 -0
- data/examples/modules.rsh +6 -0
- data/examples/paste.rsh +15 -0
- data/examples/plugin.rb +8 -0
- data/examples/power.rsh +65 -0
- data/examples/tour.rsh +38 -0
- data/ext/srsh_native/extconf.rb +3 -0
- data/ext/srsh_native/srsh_native.c +48 -0
- data/language-docs/LANGUAGE.md +670 -0
- data/language-docs/MIGRATION.md +44 -0
- data/language-docs/SECURITY.md +44 -0
- data/lib/srsh/app.rb +261 -0
- data/lib/srsh/builtins.rb +492 -0
- data/lib/srsh/editor.rb +530 -0
- data/lib/srsh/errors.rb +23 -0
- data/lib/srsh/history.rb +74 -0
- data/lib/srsh/language/evaluator.rb +1175 -0
- data/lib/srsh/language/lexer.rb +316 -0
- data/lib/srsh/language/parser.rb +997 -0
- data/lib/srsh/language/token.rb +5 -0
- data/lib/srsh/language/values.rb +392 -0
- data/lib/srsh/paths.rb +29 -0
- data/lib/srsh/plugins.rb +59 -0
- data/lib/srsh/process_identity.rb +38 -0
- data/lib/srsh/security.rb +38 -0
- data/lib/srsh/shell/executor.rb +1182 -0
- data/lib/srsh/shell/job.rb +101 -0
- data/lib/srsh/shell/lexer.rb +114 -0
- data/lib/srsh/shell/terminal.rb +26 -0
- data/lib/srsh/state.rb +136 -0
- data/lib/srsh/theme.rb +108 -0
- data/lib/srsh/version.rb +1 -2
- data/lib/srsh.rb +15 -0
- metadata +59 -11
|
@@ -0,0 +1,997 @@
|
|
|
1
|
+
require_relative 'lexer'
|
|
2
|
+
|
|
3
|
+
module Srsh
|
|
4
|
+
module Language
|
|
5
|
+
class ExprParser
|
|
6
|
+
MAX_NESTING = 256
|
|
7
|
+
MAX_BYTES = 1024 * 1024
|
|
8
|
+
PRECEDENCE = {
|
|
9
|
+
'??' => 1,
|
|
10
|
+
'or' => 2, '||' => 2,
|
|
11
|
+
'and' => 3, '&&' => 3,
|
|
12
|
+
'==' => 4, '!=' => 4, '===' => 4, '!==' => 4, '=~' => 4, '!~' => 4, 'in' => 4,
|
|
13
|
+
'<' => 5, '<=' => 5, '>' => 5, '>=' => 5,
|
|
14
|
+
'|>' => 6,
|
|
15
|
+
'..' => 7, '..<' => 7,
|
|
16
|
+
'++' => 8, '+' => 8, '-' => 8,
|
|
17
|
+
'*' => 9, '/' => 9, '%' => 9,
|
|
18
|
+
'**' => 10
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def initialize(source, line: 1)
|
|
22
|
+
source = source.to_s
|
|
23
|
+
raise ParseError.new('expression too large', line: line, column: 1) if source.bytesize > MAX_BYTES
|
|
24
|
+
@lexer = Lexer.new(source, line: line)
|
|
25
|
+
@token = @lexer.next_token
|
|
26
|
+
@depth = 0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def parse
|
|
30
|
+
ast = expression(0)
|
|
31
|
+
error("unexpected #{@token.value.inspect}") unless @token.type == :eof
|
|
32
|
+
ast
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def error(message, token = @token)
|
|
38
|
+
klass = token.type == :eof ? IncompleteInput : ParseError
|
|
39
|
+
raise klass.new(message, line: token.line, column: token.column)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def advance
|
|
43
|
+
old = @token
|
|
44
|
+
@token = @lexer.next_token
|
|
45
|
+
old
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def expect(type)
|
|
49
|
+
error("expected #{type}, got #{@token.type}") unless @token.type == type
|
|
50
|
+
advance
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def lbp(token)
|
|
54
|
+
token.type == :op ? PRECEDENCE.fetch(token.value, 0) : 0
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def expression(rbp)
|
|
58
|
+
@depth += 1
|
|
59
|
+
error('expression nesting too deep') if @depth > MAX_NESTING
|
|
60
|
+
t = advance
|
|
61
|
+
left = nud(t)
|
|
62
|
+
loop do
|
|
63
|
+
if @token.type == :lparen
|
|
64
|
+
left = parse_call(left)
|
|
65
|
+
next
|
|
66
|
+
elsif @token.type == :lbracket
|
|
67
|
+
advance
|
|
68
|
+
index = expression(0)
|
|
69
|
+
expect(:rbracket)
|
|
70
|
+
left = [:index, left, index]
|
|
71
|
+
next
|
|
72
|
+
elsif @token.type == :dot
|
|
73
|
+
advance
|
|
74
|
+
name = expect(:ident)
|
|
75
|
+
left = [:member, left, name.value]
|
|
76
|
+
next
|
|
77
|
+
elsif @token.type == :safe_dot
|
|
78
|
+
advance
|
|
79
|
+
name = expect(:ident)
|
|
80
|
+
left = [:safe_member, left, name.value]
|
|
81
|
+
next
|
|
82
|
+
elsif @token.type == :safe_lbracket
|
|
83
|
+
advance
|
|
84
|
+
index = expression(0)
|
|
85
|
+
expect(:rbracket)
|
|
86
|
+
left = [:safe_index, left, index]
|
|
87
|
+
next
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
power = lbp(@token)
|
|
91
|
+
break if power <= rbp
|
|
92
|
+
op = advance.value
|
|
93
|
+
right_power = op == '**' ? power - 1 : power
|
|
94
|
+
left = [:binary, op, left, expression(right_power)]
|
|
95
|
+
end
|
|
96
|
+
left
|
|
97
|
+
ensure
|
|
98
|
+
@depth -= 1 if @depth.positive?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def nud(t)
|
|
102
|
+
case t.type
|
|
103
|
+
when :number
|
|
104
|
+
raw = t.value.delete('_')
|
|
105
|
+
value = if raw.match?(/\A0[xX]/) then raw.to_i(16)
|
|
106
|
+
elsif raw.match?(/\A0[bB]/) then raw.to_i(2)
|
|
107
|
+
elsif raw.match?(/\A0[oO]/) then raw.to_i(8)
|
|
108
|
+
elsif raw.include?('.') || raw.match?(/[eE]/) then raw.to_f
|
|
109
|
+
else raw.to_i
|
|
110
|
+
end
|
|
111
|
+
[:literal, value]
|
|
112
|
+
when :string then [:literal, t.value]
|
|
113
|
+
when :template
|
|
114
|
+
[:template, t.value.map { |kind, value| kind == :text ? [:text, value] : [:expr, self.class.new(value, line: t.line).parse] }]
|
|
115
|
+
when :true then [:literal, true]
|
|
116
|
+
when :false then [:literal, false]
|
|
117
|
+
when :void then [:literal, nil]
|
|
118
|
+
when :ident then [:local, t.value]
|
|
119
|
+
when :env then [:env, t.value]
|
|
120
|
+
when :positional then [:positional, t.value]
|
|
121
|
+
when :status then [:status]
|
|
122
|
+
when :capture then [:capture, t.value]
|
|
123
|
+
when :lambda then parse_lambda(t)
|
|
124
|
+
when :lparen
|
|
125
|
+
value = expression(0)
|
|
126
|
+
expect(:rparen)
|
|
127
|
+
value
|
|
128
|
+
when :lbracket
|
|
129
|
+
parse_list
|
|
130
|
+
when :map_open
|
|
131
|
+
parse_map
|
|
132
|
+
when :op
|
|
133
|
+
if t.value == '&'
|
|
134
|
+
[:spawn, expression(11)]
|
|
135
|
+
elsif %w[- + not !].include?(t.value)
|
|
136
|
+
[:unary, t.value, expression(11)]
|
|
137
|
+
else
|
|
138
|
+
error("unexpected operator #{t.value.inspect}", t)
|
|
139
|
+
end
|
|
140
|
+
else
|
|
141
|
+
error("unexpected token #{t.type}", t)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def parse_lambda(token)
|
|
147
|
+
params = []
|
|
148
|
+
if @token.type == :fat_arrow
|
|
149
|
+
# zero-argument hot lambda: :: => expr
|
|
150
|
+
elsif @token.type == :lparen
|
|
151
|
+
advance
|
|
152
|
+
unless @token.type == :rparen
|
|
153
|
+
loop do
|
|
154
|
+
rest = false
|
|
155
|
+
if @token.type == :op && @token.value == '*'
|
|
156
|
+
advance
|
|
157
|
+
rest = true
|
|
158
|
+
end
|
|
159
|
+
name = expect(:ident)
|
|
160
|
+
params << (rest ? "*#{name.value}" : name.value)
|
|
161
|
+
if rest && @token.type != :rparen
|
|
162
|
+
error('rest lambda parameter must be last')
|
|
163
|
+
end
|
|
164
|
+
break if @token.type == :rparen
|
|
165
|
+
expect(:comma)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
expect(:rparen)
|
|
169
|
+
else
|
|
170
|
+
params << expect(:ident).value
|
|
171
|
+
end
|
|
172
|
+
error('expected => after lambda parameters', @token) unless @token.type == :fat_arrow
|
|
173
|
+
advance
|
|
174
|
+
[:lambda, params, expression(0)]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def parse_list
|
|
178
|
+
items = []
|
|
179
|
+
unless @token.type == :rbracket
|
|
180
|
+
loop do
|
|
181
|
+
items << expression(0)
|
|
182
|
+
break if @token.type == :rbracket
|
|
183
|
+
expect(:comma)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
expect(:rbracket)
|
|
187
|
+
[:list, items]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def parse_map
|
|
191
|
+
pairs = []
|
|
192
|
+
unless @token.type == :rbracket
|
|
193
|
+
loop do
|
|
194
|
+
key = if @token.type == :ident
|
|
195
|
+
[:literal, advance.value]
|
|
196
|
+
else
|
|
197
|
+
expression(0)
|
|
198
|
+
end
|
|
199
|
+
expect(:colon)
|
|
200
|
+
pairs << [key, expression(0)]
|
|
201
|
+
break if @token.type == :rbracket
|
|
202
|
+
expect(:comma)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
expect(:rbracket)
|
|
206
|
+
[:map, pairs]
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def parse_call(callee)
|
|
210
|
+
advance
|
|
211
|
+
args = []
|
|
212
|
+
unless @token.type == :rparen
|
|
213
|
+
loop do
|
|
214
|
+
args << expression(0)
|
|
215
|
+
break if @token.type == :rparen
|
|
216
|
+
expect(:comma)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
expect(:rparen)
|
|
220
|
+
[:call, callee, args]
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
Command = Data.define(:line, :number)
|
|
225
|
+
Assign = Data.define(:target, :op, :expr, :number)
|
|
226
|
+
DestructureNode = Data.define(:names, :expr, :number)
|
|
227
|
+
Emit = Data.define(:expr, :number)
|
|
228
|
+
ExprNode = Data.define(:expr, :number)
|
|
229
|
+
IfNode = Data.define(:cond, :yes, :no, :number)
|
|
230
|
+
LoopNode = Data.define(:expr, :name, :body, :number)
|
|
231
|
+
WhileNode = Data.define(:cond, :body, :number)
|
|
232
|
+
FunctionNode = Data.define(:name, :params, :body, :number)
|
|
233
|
+
TaskFunctionNode = Data.define(:name, :params, :body, :number)
|
|
234
|
+
ReturnNode = Data.define(:expr, :number)
|
|
235
|
+
BreakNode = Data.define(:number)
|
|
236
|
+
NextNode = Data.define(:number)
|
|
237
|
+
MatchNode = Data.define(:expr, :arms, :number)
|
|
238
|
+
MatchArm = Data.define(:pattern, :body, :number)
|
|
239
|
+
CodeNode = Data.define(:name, :source, :body, :number)
|
|
240
|
+
TryNode = Data.define(:body, :error_name, :catch_body, :finally_body, :number)
|
|
241
|
+
SpaceNode = Data.define(:name, :body, :number)
|
|
242
|
+
SlotNode = Data.define(:name, :expr, :number)
|
|
243
|
+
ProtoNode = Data.define(:name, :params, :traits, :body, :number)
|
|
244
|
+
TraitNode = Data.define(:name, :body, :number)
|
|
245
|
+
BridgeSymbol = Data.define(:name, :params, :result, :number)
|
|
246
|
+
BridgeNode = Data.define(:name, :library, :symbols, :number)
|
|
247
|
+
UseNode = Data.define(:path, :name, :number)
|
|
248
|
+
DeferNode = Data.define(:body, :number)
|
|
249
|
+
|
|
250
|
+
class ProgramParser
|
|
251
|
+
MAX_LINES = 100_000
|
|
252
|
+
MAX_NESTING = 256
|
|
253
|
+
|
|
254
|
+
def initialize(text, line_offset: 0, nesting: 0)
|
|
255
|
+
raise ParseError, 'script too large' if text.bytesize > 4 * 1024 * 1024
|
|
256
|
+
@lines = text.lines(chomp: true)
|
|
257
|
+
@line_offset = line_offset
|
|
258
|
+
@nesting = nesting
|
|
259
|
+
raise ParseError, 'script has too many lines' if @lines.length > MAX_LINES
|
|
260
|
+
raise ParseError, 'script nesting too deep' if @nesting > MAX_NESTING
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def parse
|
|
264
|
+
nodes, index, stop = parse_nodes(0, [], @nesting)
|
|
265
|
+
raise ParseError.new("unexpected #{stop}", line: @line_offset + index + 1) if stop
|
|
266
|
+
validate_nodes!(nodes)
|
|
267
|
+
nodes
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
private
|
|
271
|
+
|
|
272
|
+
def clean(line)
|
|
273
|
+
single = false
|
|
274
|
+
double = false
|
|
275
|
+
raw = false
|
|
276
|
+
escaped = false
|
|
277
|
+
out = +''
|
|
278
|
+
i = 0
|
|
279
|
+
|
|
280
|
+
while i < line.length
|
|
281
|
+
c = line[i]
|
|
282
|
+
n = line[i + 1]
|
|
283
|
+
if escaped
|
|
284
|
+
out << c
|
|
285
|
+
escaped = false
|
|
286
|
+
elsif !single && !raw && c == '\\'
|
|
287
|
+
out << c
|
|
288
|
+
escaped = true
|
|
289
|
+
elsif !single && !double && c == '[' && n == '['
|
|
290
|
+
raw = true
|
|
291
|
+
out << '[['
|
|
292
|
+
i += 1
|
|
293
|
+
elsif raw && c == ']' && n == ']'
|
|
294
|
+
raw = false
|
|
295
|
+
out << ']]'
|
|
296
|
+
i += 1
|
|
297
|
+
elsif !double && !raw && c == "'"
|
|
298
|
+
single = !single
|
|
299
|
+
out << c
|
|
300
|
+
elsif !single && !raw && c == '"'
|
|
301
|
+
double = !double
|
|
302
|
+
out << c
|
|
303
|
+
elsif c == '#' && !single && !double && !raw
|
|
304
|
+
break
|
|
305
|
+
else
|
|
306
|
+
out << c
|
|
307
|
+
end
|
|
308
|
+
i += 1
|
|
309
|
+
end
|
|
310
|
+
out.rstrip
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def parse_nodes(index, stops, depth)
|
|
314
|
+
raise ParseError.new('script nesting too deep', line: @line_offset + index + 1) if depth > MAX_NESTING
|
|
315
|
+
nodes = []
|
|
316
|
+
while index < @lines.length
|
|
317
|
+
number = @line_offset + index + 1
|
|
318
|
+
line = clean(@lines[index]).strip
|
|
319
|
+
index += 1
|
|
320
|
+
|
|
321
|
+
# Value pipelines are allowed to flow vertically. A continuation line
|
|
322
|
+
# beginning with |> is unambiguously RSH expression syntax (Unix uses
|
|
323
|
+
# bare |), so this buys readability without indentation semantics.
|
|
324
|
+
while !line.empty? && index < @lines.length
|
|
325
|
+
continuation = clean(@lines[index]).strip
|
|
326
|
+
break unless continuation.start_with?('|>')
|
|
327
|
+
line << ' ' << continuation
|
|
328
|
+
index += 1
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
next if line.empty? || (number == 1 && line.start_with?('#!'))
|
|
332
|
+
return [nodes, index - 1, line] if stops.include?(line)
|
|
333
|
+
|
|
334
|
+
case line
|
|
335
|
+
# Readable forms. These intentionally lower to the same nodes as the
|
|
336
|
+
# sigil forms below; maintained scripts and hot scripts are one language.
|
|
337
|
+
when /\Ause\s+(.+?)(?:\s+as\s+([A-Za-z_][A-Za-z0-9_]*))?\s*\z/
|
|
338
|
+
path_expr = Regexp.last_match(1).strip
|
|
339
|
+
alias_name = Regexp.last_match(2)
|
|
340
|
+
nodes << UseNode.new(path_expr, alias_name, number)
|
|
341
|
+
when 'defer'
|
|
342
|
+
body, idx, stop = parse_nodes(index, ['end'], depth + 1)
|
|
343
|
+
raise IncompleteInput.new('unclosed defer block', line: number) unless stop == 'end'
|
|
344
|
+
nodes << DeferNode.new(body, number)
|
|
345
|
+
index = idx + 1
|
|
346
|
+
when /\Adefer\s+(.+)\z/
|
|
347
|
+
nodes << DeferNode.new(parse_inline_statement(Regexp.last_match(1), number, depth), number)
|
|
348
|
+
when 'try'
|
|
349
|
+
node, index = parse_try_block(index, number, depth)
|
|
350
|
+
nodes << node
|
|
351
|
+
when /\Aif\s+(.+?)\s*=>\s*(.+)\z/
|
|
352
|
+
nodes << IfNode.new(Regexp.last_match(1).strip,
|
|
353
|
+
parse_inline_statement(Regexp.last_match(2), number, depth), [], number)
|
|
354
|
+
when /\Aif\s+(.+)\z/
|
|
355
|
+
yes, idx, stop = parse_nodes(index, ['else', 'end'], depth + 1)
|
|
356
|
+
no = []
|
|
357
|
+
if stop == 'else'
|
|
358
|
+
no, idx2, stop2 = parse_nodes(idx + 1, ['end'], depth + 1)
|
|
359
|
+
raise IncompleteInput.new('unclosed if block', line: number) unless stop2 == 'end'
|
|
360
|
+
index = idx2 + 1
|
|
361
|
+
elsif stop == 'end'
|
|
362
|
+
index = idx + 1
|
|
363
|
+
else
|
|
364
|
+
raise IncompleteInput.new('unclosed if block', line: number)
|
|
365
|
+
end
|
|
366
|
+
nodes << IfNode.new(Regexp.last_match(1).strip, yes, no, number)
|
|
367
|
+
when /\Awhile\s+(.+?)\s*=>\s*(.+)\z/
|
|
368
|
+
nodes << WhileNode.new(Regexp.last_match(1).strip,
|
|
369
|
+
parse_inline_statement(Regexp.last_match(2), number, depth), number)
|
|
370
|
+
when /\Awhile\s+(.+)\z/
|
|
371
|
+
body, idx, stop = parse_nodes(index, ['end'], depth + 1)
|
|
372
|
+
raise IncompleteInput.new('unclosed while block', line: number) unless stop == 'end'
|
|
373
|
+
nodes << WhileNode.new(Regexp.last_match(1).strip, body, number)
|
|
374
|
+
index = idx + 1
|
|
375
|
+
when /\Aeach\s+(.+?)\s*->\s*([A-Za-z_][A-Za-z0-9_]*(?:\s*,\s*[A-Za-z_][A-Za-z0-9_]*)*)\s*=>\s*(.+)\z/
|
|
376
|
+
nodes << LoopNode.new(Regexp.last_match(1).strip, parse_loop_names(Regexp.last_match(2)),
|
|
377
|
+
parse_inline_statement(Regexp.last_match(3), number, depth), number)
|
|
378
|
+
when /\Aeach\s+(.+?)\s*->\s*([A-Za-z_][A-Za-z0-9_]*(?:\s*,\s*[A-Za-z_][A-Za-z0-9_]*)*)\z/
|
|
379
|
+
body, idx, stop = parse_nodes(index, ['end'], depth + 1)
|
|
380
|
+
raise IncompleteInput.new('unclosed each block', line: number) unless stop == 'end'
|
|
381
|
+
nodes << LoopNode.new(Regexp.last_match(1).strip, parse_loop_names(Regexp.last_match(2)), body, number)
|
|
382
|
+
index = idx + 1
|
|
383
|
+
when /\Atimes\s+(.+)\z/
|
|
384
|
+
body, idx, stop = parse_nodes(index, ['end'], depth + 1)
|
|
385
|
+
raise IncompleteInput.new('unclosed times block', line: number) unless stop == 'end'
|
|
386
|
+
nodes << LoopNode.new(Regexp.last_match(1).strip, 'it', body, number)
|
|
387
|
+
index = idx + 1
|
|
388
|
+
when /\Abridge\s+([A-Za-z_][A-Za-z0-9_]*)\s+from\s+(.+)\z/
|
|
389
|
+
name = Regexp.last_match(1)
|
|
390
|
+
library = Regexp.last_match(2).strip
|
|
391
|
+
symbols, index = parse_bridge_block(index, number, depth)
|
|
392
|
+
nodes << BridgeNode.new(name, library, symbols.freeze, number)
|
|
393
|
+
when /\Aspace\s+([A-Za-z_][A-Za-z0-9_]*)\s*\z/
|
|
394
|
+
name = Regexp.last_match(1)
|
|
395
|
+
body, idx, stop = parse_nodes(index, ['end'], depth + 1)
|
|
396
|
+
raise IncompleteInput.new('unclosed space block', line: number) unless stop == 'end'
|
|
397
|
+
allowed = [Assign, DestructureNode, FunctionNode, TaskFunctionNode, ProtoNode, TraitNode, CodeNode, SpaceNode, BridgeNode, UseNode]
|
|
398
|
+
unless body.all? { |node| allowed.any? { |klass| node.is_a?(klass) } }
|
|
399
|
+
raise ParseError.new('space bodies may contain bindings and declarations only', line: number)
|
|
400
|
+
end
|
|
401
|
+
nodes << SpaceNode.new(name, body, number)
|
|
402
|
+
index = idx + 1
|
|
403
|
+
when /\Aproto\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:\((.*?)\))?\s*(?:with\s+([A-Za-z_][A-Za-z0-9_]*(?:\s*,\s*[A-Za-z_][A-Za-z0-9_]*)*))?\s*\z/
|
|
404
|
+
name = Regexp.last_match(1)
|
|
405
|
+
params = parse_params(Regexp.last_match(2).to_s, number)
|
|
406
|
+
traits = Regexp.last_match(3).to_s.split(',').map(&:strip).reject(&:empty?).freeze
|
|
407
|
+
body, idx, stop = parse_nodes(index, ['end'], depth + 1)
|
|
408
|
+
raise IncompleteInput.new('unclosed proto block', line: number) unless stop == 'end'
|
|
409
|
+
unless body.all? { |node| node.is_a?(SlotNode) || node.is_a?(FunctionNode) || node.is_a?(TaskFunctionNode) }
|
|
410
|
+
raise ParseError.new('proto bodies may contain only slot, fn, and task declarations', line: number)
|
|
411
|
+
end
|
|
412
|
+
ensure_unique_declarations!(body, number, 'prototype')
|
|
413
|
+
duplicate_trait = traits.group_by(&:itself).find { |_trait, names| names.length > 1 }&.first
|
|
414
|
+
raise ParseError.new("duplicate trait #{duplicate_trait.inspect}", line: number) if duplicate_trait
|
|
415
|
+
nodes << ProtoNode.new(name, params, traits, body, number)
|
|
416
|
+
index = idx + 1
|
|
417
|
+
when /\Atrait\s+([A-Za-z_][A-Za-z0-9_]*)\s*\z/
|
|
418
|
+
name = Regexp.last_match(1)
|
|
419
|
+
body, idx, stop = parse_nodes(index, ['end'], depth + 1)
|
|
420
|
+
raise IncompleteInput.new('unclosed trait block', line: number) unless stop == 'end'
|
|
421
|
+
unless body.all? { |node| node.is_a?(FunctionNode) || node.is_a?(TaskFunctionNode) }
|
|
422
|
+
raise ParseError.new('trait bodies may contain only fn and task declarations', line: number)
|
|
423
|
+
end
|
|
424
|
+
ensure_unique_declarations!(body, number, 'trait')
|
|
425
|
+
nodes << TraitNode.new(name, body, number)
|
|
426
|
+
index = idx + 1
|
|
427
|
+
when /\Acode\s+([A-Za-z_][A-Za-z0-9_]*)\z/
|
|
428
|
+
name = Regexp.last_match(1)
|
|
429
|
+
body_start = index
|
|
430
|
+
body, idx, stop = parse_nodes(index, ['end'], depth + 1)
|
|
431
|
+
raise IncompleteInput.new('unclosed code block', line: number) unless stop == 'end'
|
|
432
|
+
source = @lines[body_start...idx].join("\n")
|
|
433
|
+
nodes << CodeNode.new(name, source, body, number)
|
|
434
|
+
index = idx + 1
|
|
435
|
+
when /\Atask\s+([A-Za-z_][A-Za-z0-9_]*)\s*\((.*)\)\s*=>\s*\z/
|
|
436
|
+
name = Regexp.last_match(1)
|
|
437
|
+
params = parse_params(Regexp.last_match(2).to_s, number)
|
|
438
|
+
expr, index = read_continued_expression(index, number)
|
|
439
|
+
nodes << TaskFunctionNode.new(name, params, [ReturnNode.new(expr, number)], number)
|
|
440
|
+
when /\Atask\s+([A-Za-z_][A-Za-z0-9_]*)\s*\((.*)\)\s*=>\s*(.+)\z/
|
|
441
|
+
name = Regexp.last_match(1)
|
|
442
|
+
params = parse_params(Regexp.last_match(2).to_s, number)
|
|
443
|
+
nodes << TaskFunctionNode.new(name, params, [ReturnNode.new(Regexp.last_match(3).strip, number)], number)
|
|
444
|
+
when /\Atask\s+([A-Za-z_][A-Za-z0-9_]*)\s*\((.*)\)\s*\z/
|
|
445
|
+
name = Regexp.last_match(1)
|
|
446
|
+
params = parse_params(Regexp.last_match(2).to_s, number)
|
|
447
|
+
body, idx, stop = parse_nodes(index, ['end'], depth + 1)
|
|
448
|
+
raise IncompleteInput.new('unclosed task block', line: number) unless stop == 'end'
|
|
449
|
+
nodes << TaskFunctionNode.new(name, params, body, number)
|
|
450
|
+
index = idx + 1
|
|
451
|
+
when /\Afn\s+([A-Za-z_][A-Za-z0-9_]*)\s*\((.*)\)\s*=>\s*\z/
|
|
452
|
+
name = Regexp.last_match(1)
|
|
453
|
+
params = parse_params(Regexp.last_match(2).to_s, number)
|
|
454
|
+
expr, index = read_continued_expression(index, number)
|
|
455
|
+
nodes << FunctionNode.new(name, params, [ReturnNode.new(expr, number)], number)
|
|
456
|
+
when /\Afn\s+([A-Za-z_][A-Za-z0-9_]*)\s*\((.*)\)\s*=>\s*(.+)\z/
|
|
457
|
+
name = Regexp.last_match(1)
|
|
458
|
+
params = parse_params(Regexp.last_match(2).to_s, number)
|
|
459
|
+
nodes << FunctionNode.new(name, params, [ReturnNode.new(Regexp.last_match(3).strip, number)], number)
|
|
460
|
+
when /\Afn\s+([A-Za-z_][A-Za-z0-9_]*)\s*\((.*)\)\s*\z/
|
|
461
|
+
name = Regexp.last_match(1)
|
|
462
|
+
params = parse_params(Regexp.last_match(2).to_s, number)
|
|
463
|
+
body, idx, stop = parse_nodes(index, ['end'], depth + 1)
|
|
464
|
+
raise IncompleteInput.new('unclosed fn block', line: number) unless stop == 'end'
|
|
465
|
+
nodes << FunctionNode.new(name, params, body, number)
|
|
466
|
+
index = idx + 1
|
|
467
|
+
when /\Afn\s+([A-Za-z_][A-Za-z0-9_]*)\s*(.*)\z/
|
|
468
|
+
name = Regexp.last_match(1)
|
|
469
|
+
args = Regexp.last_match(2).to_s.split(/\s+/).reject(&:empty?).map { |arg| [arg, nil] }
|
|
470
|
+
body, idx, stop = parse_nodes(index, ['end'], depth + 1)
|
|
471
|
+
raise IncompleteInput.new('unclosed fn block', line: number) unless stop == 'end'
|
|
472
|
+
nodes << FunctionNode.new(name, args, body, number)
|
|
473
|
+
index = idx + 1
|
|
474
|
+
when /\Amatch\s+(.+)\z/
|
|
475
|
+
arms, index = parse_match_block(index, number, Regexp.last_match(1).strip, 'end', depth)
|
|
476
|
+
nodes << MatchNode.new(Regexp.last_match(1).strip, arms, number)
|
|
477
|
+
|
|
478
|
+
# Hot forms: tiny, visually distinct and deliberately shell-safe.
|
|
479
|
+
when /\A\?\s+(.+?)\s*=>\s*(.+)\z/
|
|
480
|
+
nodes << IfNode.new(Regexp.last_match(1).strip,
|
|
481
|
+
parse_inline_statement(Regexp.last_match(2), number, depth), [], number)
|
|
482
|
+
when /\A\?\s+(.+)\z/
|
|
483
|
+
yes, idx, stop = parse_nodes(index, [':?', '.?'], depth + 1)
|
|
484
|
+
no = []
|
|
485
|
+
if stop == ':?'
|
|
486
|
+
no, idx2, stop2 = parse_nodes(idx + 1, ['.?'], depth + 1)
|
|
487
|
+
raise IncompleteInput.new('unclosed ? block', line: number) unless stop2 == '.?'
|
|
488
|
+
index = idx2 + 1
|
|
489
|
+
elsif stop == '.?'
|
|
490
|
+
index = idx + 1
|
|
491
|
+
else
|
|
492
|
+
raise IncompleteInput.new('unclosed ? block', line: number)
|
|
493
|
+
end
|
|
494
|
+
nodes << IfNode.new(Regexp.last_match(1).strip, yes, no, number)
|
|
495
|
+
when /\A@\?\s+(.+?)\s*=>\s*(.+)\z/
|
|
496
|
+
nodes << WhileNode.new(Regexp.last_match(1).strip,
|
|
497
|
+
parse_inline_statement(Regexp.last_match(2), number, depth), number)
|
|
498
|
+
when /\A@\?\s+(.+)\z/
|
|
499
|
+
body, idx, stop = parse_nodes(index, ['.@'], depth + 1)
|
|
500
|
+
raise IncompleteInput.new('unclosed @? block', line: number) unless stop == '.@'
|
|
501
|
+
nodes << WhileNode.new(Regexp.last_match(1).strip, body, number)
|
|
502
|
+
index = idx + 1
|
|
503
|
+
when /\A@\s+(.+?)\s*->\s*([A-Za-z_][A-Za-z0-9_]*(?:\s*,\s*[A-Za-z_][A-Za-z0-9_]*)*)\s*=>\s*(.+)\z/
|
|
504
|
+
nodes << LoopNode.new(Regexp.last_match(1).strip, parse_loop_names(Regexp.last_match(2)),
|
|
505
|
+
parse_inline_statement(Regexp.last_match(3), number, depth), number)
|
|
506
|
+
when /\A@\s+(.+?)\s*->\s*([A-Za-z_][A-Za-z0-9_]*(?:\s*,\s*[A-Za-z_][A-Za-z0-9_]*)*)\z/
|
|
507
|
+
body, idx, stop = parse_nodes(index, ['.@'], depth + 1)
|
|
508
|
+
raise IncompleteInput.new('unclosed @ block', line: number) unless stop == '.@'
|
|
509
|
+
nodes << LoopNode.new(Regexp.last_match(1).strip, parse_loop_names(Regexp.last_match(2)), body, number)
|
|
510
|
+
index = idx + 1
|
|
511
|
+
when /\A::\s*([A-Za-z_][A-Za-z0-9_]*)\s*\((.*)\)\s*=>\s*(.+)\z/
|
|
512
|
+
name = Regexp.last_match(1)
|
|
513
|
+
params = parse_params(Regexp.last_match(2).to_s, number)
|
|
514
|
+
nodes << FunctionNode.new(name, params, [ReturnNode.new(Regexp.last_match(3).strip, number)], number)
|
|
515
|
+
when /\A::\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?:\((.*)\))?\s*\z/
|
|
516
|
+
name = Regexp.last_match(1)
|
|
517
|
+
params = parse_params(Regexp.last_match(2).to_s, number)
|
|
518
|
+
body, idx, stop = parse_nodes(index, ['.::'], depth + 1)
|
|
519
|
+
raise IncompleteInput.new('unclosed :: block', line: number) unless stop == '.::'
|
|
520
|
+
nodes << FunctionNode.new(name, params, body, number)
|
|
521
|
+
index = idx + 1
|
|
522
|
+
when /\A\?\?\s+(.+)\z/
|
|
523
|
+
match_expr = Regexp.last_match(1).strip
|
|
524
|
+
arms, index = parse_match_block(index, number, match_expr, '.??', depth)
|
|
525
|
+
nodes << MatchNode.new(match_expr, arms, number)
|
|
526
|
+
|
|
527
|
+
when 'break', '^!'
|
|
528
|
+
nodes << BreakNode.new(number)
|
|
529
|
+
when 'continue', '^>'
|
|
530
|
+
nodes << NextNode.new(number)
|
|
531
|
+
when /\Areturn(?:\s+(.*))?\z/
|
|
532
|
+
expr = Regexp.last_match(1).to_s.strip
|
|
533
|
+
expr, index = complete_expression(expr, index, number) unless expr.empty?
|
|
534
|
+
nodes << ReturnNode.new(expr, number)
|
|
535
|
+
when /\A\^\s*(.*)\z/
|
|
536
|
+
nodes << ReturnNode.new(Regexp.last_match(1).strip, number)
|
|
537
|
+
when /\A=\s*(.+)\z/
|
|
538
|
+
expr, index = complete_expression(Regexp.last_match(1), index, number)
|
|
539
|
+
nodes << Emit.new(expr, number)
|
|
540
|
+
when /\Aemit\s+(.+)\z/
|
|
541
|
+
expr, index = complete_expression(Regexp.last_match(1), index, number)
|
|
542
|
+
nodes << Emit.new(expr, number)
|
|
543
|
+
when /\Aslot\s+([A-Za-z_][A-Za-z0-9_]*)\s*:=\s*(.+)\z/
|
|
544
|
+
slot_name, rhs = Regexp.last_match(1), Regexp.last_match(2)
|
|
545
|
+
rhs, index = complete_expression(rhs, index, number)
|
|
546
|
+
nodes << SlotNode.new(slot_name, rhs, number)
|
|
547
|
+
when /\A((?:\*?[A-Za-z_][A-Za-z0-9_]*\s*,\s*)+\*?[A-Za-z_][A-Za-z0-9_]*)\s*:=\s*(.+)\z/
|
|
548
|
+
names = Regexp.last_match(1).split(',').map(&:strip)
|
|
549
|
+
rest = names.each_index.select { |i| names[i].start_with?('*') }
|
|
550
|
+
raise ParseError.new('destructuring allows one rest name, and it must be last', line: number) if rest.length > 1 || (rest.any? && rest[0] != names.length - 1)
|
|
551
|
+
rhs, index = complete_expression(Regexp.last_match(2), index, number)
|
|
552
|
+
nodes << DestructureNode.new(names.freeze, rhs, number)
|
|
553
|
+
when /\A((?:[A-Za-z_][A-Za-z0-9_]*)(?:\.[A-Za-z_][A-Za-z0-9_]*)+)\s*(:=|\+=|-=|\*=|\/=|%=|\+\+=)\s*(.+)\z/
|
|
554
|
+
target, op, rhs = Regexp.last_match(1), Regexp.last_match(2), Regexp.last_match(3)
|
|
555
|
+
rhs, index = complete_expression(rhs, index, number)
|
|
556
|
+
nodes << Assign.new(target, op, rhs, number)
|
|
557
|
+
when /\A(\$?[A-Za-z_][A-Za-z0-9_]*)\s*(:=|\+=|-=|\*=|\/=|%=|\+\+=)\s*(.+)\z/
|
|
558
|
+
target, op, rhs = Regexp.last_match(1), Regexp.last_match(2), Regexp.last_match(3)
|
|
559
|
+
rhs, index = complete_expression(rhs, index, number)
|
|
560
|
+
nodes << Assign.new(target, op, rhs, number)
|
|
561
|
+
when /\A([A-Za-z_][A-Za-z0-9_]*)\s+=\s+(.+)\z/
|
|
562
|
+
name, rhs = Regexp.last_match(1), Regexp.last_match(2)
|
|
563
|
+
rhs, index = complete_expression(rhs, index, number)
|
|
564
|
+
nodes << Assign.new("$#{name}", ':=', rhs, number)
|
|
565
|
+
when /\A[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*\s*\(.*\)\s*\z/
|
|
566
|
+
nodes << ExprNode.new(line, number)
|
|
567
|
+
when /\|>/
|
|
568
|
+
# A bare value pipeline is an expression statement. Keeping this
|
|
569
|
+
# explicit avoids stealing ordinary shell commands such as `ls -la`.
|
|
570
|
+
ExprParser.new(line, line: number).parse
|
|
571
|
+
nodes << ExprNode.new(line, number)
|
|
572
|
+
else
|
|
573
|
+
if line.start_with?('.?', '.@', '.::', '.??', ':?', '| ') || %w[else end].include?(line)
|
|
574
|
+
return [nodes, index - 1, line]
|
|
575
|
+
end
|
|
576
|
+
nodes << Command.new(line, number)
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
[nodes, index, nil]
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
def parse_loop_names(text)
|
|
583
|
+
names = text.to_s.split(',').map(&:strip)
|
|
584
|
+
return names.first if names.length == 1
|
|
585
|
+
raise ParseError, 'loop destructuring supports at most 8 names' if names.length > 8
|
|
586
|
+
names.freeze
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def parse_inline_statement(text, number, depth)
|
|
590
|
+
parsed = self.class.new(text.to_s + "\n", line_offset: number - 1, nesting: depth + 1).parse
|
|
591
|
+
raise ParseError.new('inline form requires one statement', line: number) unless parsed.length == 1
|
|
592
|
+
parsed
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def parse_try_block(index, number, depth)
|
|
596
|
+
body_start = index
|
|
597
|
+
catch_start = nil
|
|
598
|
+
catch_end = nil
|
|
599
|
+
catch_name = nil
|
|
600
|
+
finally_start = nil
|
|
601
|
+
finally_end = nil
|
|
602
|
+
body_end = nil
|
|
603
|
+
nested = 0
|
|
604
|
+
phase = :body
|
|
605
|
+
closed = false
|
|
606
|
+
|
|
607
|
+
while index < @lines.length
|
|
608
|
+
candidate = clean(@lines[index]).strip
|
|
609
|
+
|
|
610
|
+
if nested.zero?
|
|
611
|
+
if candidate =~ /\Acatch(?:\s+([A-Za-z_][A-Za-z0-9_]*))?\s*\z/
|
|
612
|
+
raise ParseError.new('try block has more than one catch', line: @line_offset + index + 1) if catch_start
|
|
613
|
+
raise ParseError.new('catch must appear before finally', line: @line_offset + index + 1) if phase == :finally
|
|
614
|
+
body_end ||= index
|
|
615
|
+
catch_name = Regexp.last_match(1) || 'error'
|
|
616
|
+
catch_start = index + 1
|
|
617
|
+
phase = :catch
|
|
618
|
+
index += 1
|
|
619
|
+
next
|
|
620
|
+
elsif candidate == 'finally'
|
|
621
|
+
raise ParseError.new('try block has more than one finally', line: @line_offset + index + 1) if finally_start
|
|
622
|
+
if phase == :body
|
|
623
|
+
body_end ||= index
|
|
624
|
+
elsif phase == :catch
|
|
625
|
+
catch_end = index
|
|
626
|
+
end
|
|
627
|
+
finally_start = index + 1
|
|
628
|
+
phase = :finally
|
|
629
|
+
index += 1
|
|
630
|
+
next
|
|
631
|
+
elsif candidate == 'end'
|
|
632
|
+
if phase == :body
|
|
633
|
+
body_end ||= index
|
|
634
|
+
elsif phase == :catch
|
|
635
|
+
catch_end ||= index
|
|
636
|
+
else
|
|
637
|
+
finally_end = index
|
|
638
|
+
end
|
|
639
|
+
closed = true
|
|
640
|
+
index += 1
|
|
641
|
+
break
|
|
642
|
+
end
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
if block_opener?(candidate)
|
|
646
|
+
nested += 1
|
|
647
|
+
elsif block_closer?(candidate) && nested.positive?
|
|
648
|
+
nested -= 1
|
|
649
|
+
end
|
|
650
|
+
index += 1
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
raise IncompleteInput.new('unclosed try block', line: number) unless closed
|
|
654
|
+
raise ParseError.new('try needs catch and/or finally', line: number) unless catch_start || finally_start
|
|
655
|
+
|
|
656
|
+
body_text = @lines[body_start...(body_end || body_start)].join("\n")
|
|
657
|
+
body = self.class.new(body_text, line_offset: @line_offset + body_start, nesting: depth + 1).parse
|
|
658
|
+
|
|
659
|
+
catch_body = []
|
|
660
|
+
if catch_start
|
|
661
|
+
last = catch_end || (finally_start ? finally_start - 1 : index - 1)
|
|
662
|
+
catch_text = @lines[catch_start...last].join("\n")
|
|
663
|
+
catch_body = self.class.new(catch_text, line_offset: @line_offset + catch_start, nesting: depth + 1).parse
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
finally_body = []
|
|
667
|
+
if finally_start
|
|
668
|
+
last = finally_end || index - 1
|
|
669
|
+
finally_text = @lines[finally_start...last].join("\n")
|
|
670
|
+
finally_body = self.class.new(finally_text, line_offset: @line_offset + finally_start, nesting: depth + 1).parse
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
[TryNode.new(body, catch_name, catch_body, finally_body, number), index]
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
def read_continued_expression(index, number)
|
|
677
|
+
while index < @lines.length && clean(@lines[index]).strip.empty?
|
|
678
|
+
index += 1
|
|
679
|
+
end
|
|
680
|
+
raise IncompleteInput.new('expected expression after =>', line: number) if index >= @lines.length
|
|
681
|
+
|
|
682
|
+
expression = clean(@lines[index]).strip
|
|
683
|
+
index += 1
|
|
684
|
+
complete_expression(expression, index, number)
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
# Expression statements can span physical lines whenever the parser says
|
|
688
|
+
# it reached EOF too early. This is what makes pasted list/map/call literals
|
|
689
|
+
# work at the REPL without an indentation grammar or backslash ceremony.
|
|
690
|
+
def complete_expression(expression, index, number)
|
|
691
|
+
text = expression.to_s.strip
|
|
692
|
+
loop do
|
|
693
|
+
begin
|
|
694
|
+
ExprParser.new(text, line: number).parse
|
|
695
|
+
return [text, index]
|
|
696
|
+
rescue IncompleteInput
|
|
697
|
+
raise if index >= @lines.length
|
|
698
|
+
continuation = clean(@lines[index]).strip
|
|
699
|
+
index += 1
|
|
700
|
+
next if continuation.empty?
|
|
701
|
+
text << " " << continuation
|
|
702
|
+
end
|
|
703
|
+
end
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
def parse_bridge_block(index, number, depth)
|
|
707
|
+
raise ParseError.new('script nesting too deep', line: number) if depth > MAX_NESTING
|
|
708
|
+
symbols = []
|
|
709
|
+
names = {}
|
|
710
|
+
closed = false
|
|
711
|
+
while index < @lines.length
|
|
712
|
+
line_no = @line_offset + index + 1
|
|
713
|
+
line = clean(@lines[index]).strip
|
|
714
|
+
index += 1
|
|
715
|
+
next if line.empty?
|
|
716
|
+
if line == 'end'
|
|
717
|
+
closed = true
|
|
718
|
+
break
|
|
719
|
+
end
|
|
720
|
+
match = line.match(/\A([A-Za-z_][A-Za-z0-9_]*)\s*\((.*?)\)\s*->\s*([A-Za-z_][A-Za-z0-9_]*)\s*\z/)
|
|
721
|
+
unless match
|
|
722
|
+
raise ParseError.new('bridge entries use name(type, ...) -> type', line: line_no)
|
|
723
|
+
end
|
|
724
|
+
name = match[1]
|
|
725
|
+
raise ParseError.new("duplicate bridge symbol #{name.inspect}", line: line_no) if names[name]
|
|
726
|
+
names[name] = true
|
|
727
|
+
params = match[2].strip.empty? ? [] : split_top_level(match[2], ',').map(&:strip)
|
|
728
|
+
result = match[3]
|
|
729
|
+
symbols << BridgeSymbol.new(name, params.freeze, result, line_no)
|
|
730
|
+
end
|
|
731
|
+
raise IncompleteInput.new('unclosed bridge block', line: number) unless closed
|
|
732
|
+
[symbols, index]
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
def parse_match_block(index, number, match_expr, close_token, depth)
|
|
736
|
+
arms = []
|
|
737
|
+
closed = false
|
|
738
|
+
|
|
739
|
+
while index < @lines.length
|
|
740
|
+
while index < @lines.length && clean(@lines[index]).strip.empty?
|
|
741
|
+
index += 1
|
|
742
|
+
end
|
|
743
|
+
break if index >= @lines.length
|
|
744
|
+
|
|
745
|
+
arm_line = clean(@lines[index]).strip
|
|
746
|
+
arm_no = @line_offset + index + 1
|
|
747
|
+
if arm_line == close_token
|
|
748
|
+
index += 1
|
|
749
|
+
closed = true
|
|
750
|
+
break
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
if arm_line =~ /\A\|(?!>)\s*(.+?)\s*=>\s*(.+)\z/
|
|
754
|
+
pattern = Regexp.last_match(1).strip
|
|
755
|
+
body = parse_inline_statement(Regexp.last_match(2), arm_no, depth + 1)
|
|
756
|
+
arms << MatchArm.new(pattern, body, arm_no)
|
|
757
|
+
index += 1
|
|
758
|
+
next
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
# A match arm can put its body inline with =>, or put the body on
|
|
762
|
+
# following lines with either => or ->. Treating bare => as an
|
|
763
|
+
# incomplete arm was a nasty paste-time footgun: the readable form
|
|
764
|
+
# looked valid, but only the old arrow spelling accepted a block.
|
|
765
|
+
unless arm_line =~ /\A\|(?!>)\s*(.+?)\s*(?:->|=>)\s*\z/
|
|
766
|
+
raise ParseError.new('expected | pattern => statement or | pattern =>/-> block', line: arm_no)
|
|
767
|
+
end
|
|
768
|
+
pattern = Regexp.last_match(1).strip
|
|
769
|
+
index += 1
|
|
770
|
+
|
|
771
|
+
body_start = index
|
|
772
|
+
nested = 0
|
|
773
|
+
while index < @lines.length
|
|
774
|
+
candidate = clean(@lines[index]).strip
|
|
775
|
+
arm_marker = candidate.match?(/\A\|(?!>)\s*.+?\s*(?:->|=>)/)
|
|
776
|
+
break if nested.zero? && (candidate == close_token || arm_marker)
|
|
777
|
+
nested += 1 if block_opener?(candidate)
|
|
778
|
+
nested -= 1 if block_closer?(candidate) && nested.positive?
|
|
779
|
+
index += 1
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
body_text = @lines[body_start...index].join("\n")
|
|
783
|
+
body = self.class.new(body_text, line_offset: @line_offset + body_start, nesting: depth + 1).parse
|
|
784
|
+
arms << MatchArm.new(pattern, body, arm_no)
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
raise ParseError.new('empty match block', line: number) if arms.empty?
|
|
788
|
+
raise IncompleteInput.new("unclosed #{close_token == '.??' ? '??' : 'match'} block", line: number) unless closed
|
|
789
|
+
[arms, index]
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
def block_opener?(line)
|
|
793
|
+
return false if line.include?('=>')
|
|
794
|
+
line.match?(/\A(?:try$|defer$|if\s+|while\s+|times\s+|each\s+|code\s+|bridge\s+|space\s+|proto\s+|trait\s+|task\s+|fn\s+|match\s+|\?\s+|@\?\s+|@\s+|::\s*|\?\?\s+)/)
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
def block_closer?(line)
|
|
798
|
+
%w[end .? .@ .:: .??].include?(line)
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
def ensure_unique_declarations!(nodes, line, owner)
|
|
802
|
+
seen = {}
|
|
803
|
+
nodes.each do |node|
|
|
804
|
+
next unless node.respond_to?(:name)
|
|
805
|
+
key = node.name.to_s
|
|
806
|
+
raise ParseError.new("duplicate #{owner} declaration #{key.inspect}", line: node.number || line) if seen[key]
|
|
807
|
+
seen[key] = true
|
|
808
|
+
end
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
def validate_nodes!(nodes)
|
|
812
|
+
nodes.each do |node|
|
|
813
|
+
case node
|
|
814
|
+
when Assign
|
|
815
|
+
validate_expr!(node.expr, node.number)
|
|
816
|
+
when DestructureNode
|
|
817
|
+
validate_expr!(node.expr, node.number)
|
|
818
|
+
when Emit, ExprNode
|
|
819
|
+
validate_expr!(node.expr, node.number)
|
|
820
|
+
when IfNode
|
|
821
|
+
validate_expr!(node.cond, node.number)
|
|
822
|
+
validate_nodes!(node.yes)
|
|
823
|
+
validate_nodes!(node.no)
|
|
824
|
+
when LoopNode
|
|
825
|
+
validate_expr!(node.expr, node.number)
|
|
826
|
+
validate_nodes!(node.body)
|
|
827
|
+
when WhileNode
|
|
828
|
+
validate_expr!(node.cond, node.number)
|
|
829
|
+
validate_nodes!(node.body)
|
|
830
|
+
when FunctionNode, TaskFunctionNode
|
|
831
|
+
node.params.each { |_name, default| validate_expr!(default, node.number) if default && !default.empty? }
|
|
832
|
+
validate_nodes!(node.body)
|
|
833
|
+
when ReturnNode
|
|
834
|
+
validate_expr!(node.expr, node.number) unless node.expr.empty?
|
|
835
|
+
when MatchNode
|
|
836
|
+
validate_expr!(node.expr, node.number)
|
|
837
|
+
node.arms.each do |arm|
|
|
838
|
+
unless arm.pattern == '_'
|
|
839
|
+
pattern_expr = if arm.pattern.start_with?('? ')
|
|
840
|
+
arm.pattern[2..].strip
|
|
841
|
+
elsif arm.pattern.start_with?('when ')
|
|
842
|
+
arm.pattern[5..].strip
|
|
843
|
+
else
|
|
844
|
+
arm.pattern
|
|
845
|
+
end
|
|
846
|
+
validate_expr!(pattern_expr, arm.number)
|
|
847
|
+
end
|
|
848
|
+
validate_nodes!(arm.body)
|
|
849
|
+
end
|
|
850
|
+
when CodeNode
|
|
851
|
+
validate_nodes!(node.body)
|
|
852
|
+
when TryNode
|
|
853
|
+
validate_nodes!(node.body)
|
|
854
|
+
validate_nodes!(node.catch_body)
|
|
855
|
+
validate_nodes!(node.finally_body)
|
|
856
|
+
when SpaceNode
|
|
857
|
+
validate_nodes!(node.body)
|
|
858
|
+
when SlotNode
|
|
859
|
+
validate_expr!(node.expr, node.number)
|
|
860
|
+
when ProtoNode
|
|
861
|
+
node.params.each { |_name, default| validate_expr!(default, node.number) if default && !default.empty? }
|
|
862
|
+
validate_nodes!(node.body)
|
|
863
|
+
when TraitNode
|
|
864
|
+
validate_nodes!(node.body)
|
|
865
|
+
when BridgeNode
|
|
866
|
+
validate_expr!(node.library, node.number)
|
|
867
|
+
when UseNode
|
|
868
|
+
validate_expr!(node.path, node.number)
|
|
869
|
+
when DeferNode
|
|
870
|
+
validate_nodes!(node.body)
|
|
871
|
+
end
|
|
872
|
+
end
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
def validate_expr!(expr, line)
|
|
876
|
+
ExprParser.new(expr, line: line).parse
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
def parse_params(text, line)
|
|
880
|
+
return [] if text.strip.empty?
|
|
881
|
+
parts = split_top_level(text, ',')
|
|
882
|
+
seen_rest = false
|
|
883
|
+
seen_names = {}
|
|
884
|
+
parts.each_with_index.map do |part, index|
|
|
885
|
+
assignment = find_top_level(part, ':=')
|
|
886
|
+
if assignment
|
|
887
|
+
name = part[0...assignment].strip
|
|
888
|
+
default = part[(assignment + 2)..].strip
|
|
889
|
+
raise ParseError.new('missing default parameter expression', line: line) if default.empty?
|
|
890
|
+
else
|
|
891
|
+
name = part.strip
|
|
892
|
+
default = nil
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
rest = name.start_with?('*')
|
|
896
|
+
bare = rest ? name[1..] : name
|
|
897
|
+
unless bare&.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
|
|
898
|
+
raise ParseError.new("bad parameter #{name.inspect}", line: line)
|
|
899
|
+
end
|
|
900
|
+
raise ParseError.new("duplicate parameter #{bare.inspect}", line: line) if seen_names[bare]
|
|
901
|
+
seen_names[bare] = true
|
|
902
|
+
if rest
|
|
903
|
+
raise ParseError.new('rest parameter cannot have a default', line: line) if default
|
|
904
|
+
raise ParseError.new('rest parameter must be last', line: line) unless index == parts.length - 1
|
|
905
|
+
raise ParseError.new('only one rest parameter is allowed', line: line) if seen_rest
|
|
906
|
+
seen_rest = true
|
|
907
|
+
name = "*#{bare}"
|
|
908
|
+
end
|
|
909
|
+
[name, default]
|
|
910
|
+
end
|
|
911
|
+
end
|
|
912
|
+
|
|
913
|
+
def split_top_level(text, delimiter)
|
|
914
|
+
parts = []
|
|
915
|
+
start = 0
|
|
916
|
+
depth = 0
|
|
917
|
+
quote = nil
|
|
918
|
+
raw = false
|
|
919
|
+
escaped = false
|
|
920
|
+
i = 0
|
|
921
|
+
while i < text.length
|
|
922
|
+
c = text[i]
|
|
923
|
+
n = text[i + 1]
|
|
924
|
+
if escaped
|
|
925
|
+
escaped = false
|
|
926
|
+
elsif quote
|
|
927
|
+
if c == '\\' && quote == '"'
|
|
928
|
+
escaped = true
|
|
929
|
+
elsif c == quote
|
|
930
|
+
quote = nil
|
|
931
|
+
end
|
|
932
|
+
elsif raw
|
|
933
|
+
if c == ']' && n == ']'
|
|
934
|
+
raw = false
|
|
935
|
+
i += 1
|
|
936
|
+
end
|
|
937
|
+
elsif c == '[' && n == '['
|
|
938
|
+
raw = true
|
|
939
|
+
i += 1
|
|
940
|
+
elsif c == "'" || c == '"'
|
|
941
|
+
quote = c
|
|
942
|
+
elsif c == '(' || c == '['
|
|
943
|
+
depth += 1
|
|
944
|
+
elsif c == ')' || c == ']'
|
|
945
|
+
depth -= 1 if depth.positive?
|
|
946
|
+
elsif c == delimiter && depth.zero?
|
|
947
|
+
parts << text[start...i]
|
|
948
|
+
start = i + 1
|
|
949
|
+
end
|
|
950
|
+
i += 1
|
|
951
|
+
end
|
|
952
|
+
raise IncompleteInput, 'unterminated parameter expression' if quote || raw || depth.positive?
|
|
953
|
+
parts << text[start..]
|
|
954
|
+
parts
|
|
955
|
+
end
|
|
956
|
+
|
|
957
|
+
def find_top_level(text, needle)
|
|
958
|
+
depth = 0
|
|
959
|
+
quote = nil
|
|
960
|
+
raw = false
|
|
961
|
+
escaped = false
|
|
962
|
+
i = 0
|
|
963
|
+
while i < text.length - 1
|
|
964
|
+
c = text[i]
|
|
965
|
+
n = text[i + 1]
|
|
966
|
+
if escaped
|
|
967
|
+
escaped = false
|
|
968
|
+
elsif quote
|
|
969
|
+
if c == '\\' && quote == '"'
|
|
970
|
+
escaped = true
|
|
971
|
+
elsif c == quote
|
|
972
|
+
quote = nil
|
|
973
|
+
end
|
|
974
|
+
elsif raw
|
|
975
|
+
if c == ']' && n == ']'
|
|
976
|
+
raw = false
|
|
977
|
+
i += 1
|
|
978
|
+
end
|
|
979
|
+
elsif c == '[' && n == '['
|
|
980
|
+
raw = true
|
|
981
|
+
i += 1
|
|
982
|
+
elsif c == "'" || c == '"'
|
|
983
|
+
quote = c
|
|
984
|
+
elsif c == '(' || c == '['
|
|
985
|
+
depth += 1
|
|
986
|
+
elsif c == ')' || c == ']'
|
|
987
|
+
depth -= 1 if depth.positive?
|
|
988
|
+
elsif depth.zero? && text[i, needle.length] == needle
|
|
989
|
+
return i
|
|
990
|
+
end
|
|
991
|
+
i += 1
|
|
992
|
+
end
|
|
993
|
+
nil
|
|
994
|
+
end
|
|
995
|
+
end
|
|
996
|
+
end
|
|
997
|
+
end
|