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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +18 -14
  3. data/README.md +351 -134
  4. data/bin/srsh +53 -1473
  5. data/docs/assets/slut.txt +4 -0
  6. data/docs/assets/srsh-mark.svg +12 -0
  7. data/docs/css/style.css +696 -0
  8. data/docs/index.html +703 -0
  9. data/docs/js/app.js +203 -0
  10. data/examples/bridge.rsh +8 -0
  11. data/examples/calculator.rsh +253 -0
  12. data/examples/defer.rsh +14 -0
  13. data/examples/hot.rsh +14 -0
  14. data/examples/meta.rsh +20 -0
  15. data/examples/modules/text.rsh +6 -0
  16. data/examples/modules.rsh +6 -0
  17. data/examples/paste.rsh +15 -0
  18. data/examples/plugin.rb +8 -0
  19. data/examples/power.rsh +65 -0
  20. data/examples/tour.rsh +38 -0
  21. data/ext/srsh_native/extconf.rb +3 -0
  22. data/ext/srsh_native/srsh_native.c +48 -0
  23. data/language-docs/LANGUAGE.md +670 -0
  24. data/language-docs/MIGRATION.md +44 -0
  25. data/language-docs/SECURITY.md +44 -0
  26. data/lib/srsh/app.rb +261 -0
  27. data/lib/srsh/builtins.rb +492 -0
  28. data/lib/srsh/editor.rb +530 -0
  29. data/lib/srsh/errors.rb +23 -0
  30. data/lib/srsh/history.rb +74 -0
  31. data/lib/srsh/language/evaluator.rb +1175 -0
  32. data/lib/srsh/language/lexer.rb +316 -0
  33. data/lib/srsh/language/parser.rb +997 -0
  34. data/lib/srsh/language/token.rb +5 -0
  35. data/lib/srsh/language/values.rb +392 -0
  36. data/lib/srsh/paths.rb +29 -0
  37. data/lib/srsh/plugins.rb +59 -0
  38. data/lib/srsh/process_identity.rb +38 -0
  39. data/lib/srsh/security.rb +38 -0
  40. data/lib/srsh/shell/executor.rb +1182 -0
  41. data/lib/srsh/shell/job.rb +101 -0
  42. data/lib/srsh/shell/lexer.rb +114 -0
  43. data/lib/srsh/shell/terminal.rb +26 -0
  44. data/lib/srsh/state.rb +136 -0
  45. data/lib/srsh/theme.rb +108 -0
  46. data/lib/srsh/version.rb +1 -2
  47. data/lib/srsh.rb +15 -0
  48. 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