t-ruby 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,942 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRuby
4
+ module ParserCombinator
5
+ # Parse result - either success or failure
6
+ class ParseResult
7
+ attr_reader :value, :remaining, :position, :error
8
+
9
+ def initialize(success:, value: nil, remaining: "", position: 0, error: nil)
10
+ @success = success
11
+ @value = value
12
+ @remaining = remaining
13
+ @position = position
14
+ @error = error
15
+ end
16
+
17
+ def success?
18
+ @success
19
+ end
20
+
21
+ def failure?
22
+ !@success
23
+ end
24
+
25
+ def self.success(value, remaining, position)
26
+ new(success: true, value: value, remaining: remaining, position: position)
27
+ end
28
+
29
+ def self.failure(error, remaining, position)
30
+ new(success: false, error: error, remaining: remaining, position: position)
31
+ end
32
+
33
+ def map
34
+ return self if failure?
35
+ ParseResult.success(yield(value), remaining, position)
36
+ end
37
+
38
+ def flat_map
39
+ return self if failure?
40
+ yield(value, remaining, position)
41
+ end
42
+ end
43
+
44
+ # Base parser class
45
+ class Parser
46
+ def parse(input, position = 0)
47
+ raise NotImplementedError
48
+ end
49
+
50
+ # Combinators as methods
51
+
52
+ # Sequence: run this parser, then the other
53
+ def >>(other)
54
+ Sequence.new(self, other)
55
+ end
56
+
57
+ # Alternative: try this parser, if it fails try the other
58
+ def |(other)
59
+ Alternative.new(self, other)
60
+ end
61
+
62
+ # Map: transform the result
63
+ def map(&block)
64
+ Map.new(self, block)
65
+ end
66
+
67
+ # FlatMap: transform with another parser
68
+ def flat_map(&block)
69
+ FlatMap.new(self, block)
70
+ end
71
+
72
+ # Many: zero or more repetitions
73
+ def many
74
+ Many.new(self)
75
+ end
76
+
77
+ # Many1: one or more repetitions
78
+ def many1
79
+ Many1.new(self)
80
+ end
81
+
82
+ # Optional: zero or one
83
+ def optional
84
+ Optional.new(self)
85
+ end
86
+
87
+ # Separated by: parse items separated by delimiter
88
+ def sep_by(delimiter)
89
+ SepBy.new(self, delimiter)
90
+ end
91
+
92
+ # Separated by 1: at least one item
93
+ def sep_by1(delimiter)
94
+ SepBy1.new(self, delimiter)
95
+ end
96
+
97
+ # Between: parse between left and right delimiters
98
+ def between(left, right)
99
+ (left >> self << right).map { |(_, val)| val }
100
+ end
101
+
102
+ # Skip right: parse both, keep left result
103
+ def <<(other)
104
+ SkipRight.new(self, other)
105
+ end
106
+
107
+ # Label: add a descriptive label for error messages
108
+ def label(name)
109
+ Label.new(self, name)
110
+ end
111
+
112
+ # Lookahead: check without consuming
113
+ def lookahead
114
+ Lookahead.new(self)
115
+ end
116
+
117
+ # Not: succeed only if parser fails
118
+ def not_followed_by
119
+ NotFollowedBy.new(self)
120
+ end
121
+ end
122
+
123
+ #==========================================================================
124
+ # Primitive Parsers
125
+ #==========================================================================
126
+
127
+ # Parse a literal string
128
+ class Literal < Parser
129
+ def initialize(string)
130
+ @string = string
131
+ end
132
+
133
+ def parse(input, position = 0)
134
+ remaining = input[position..]
135
+ if remaining&.start_with?(@string)
136
+ ParseResult.success(@string, input, position + @string.length)
137
+ else
138
+ ParseResult.failure("Expected '#{@string}'", input, position)
139
+ end
140
+ end
141
+ end
142
+
143
+ # Parse a single character matching predicate
144
+ class Satisfy < Parser
145
+ def initialize(predicate, description = "character")
146
+ @predicate = predicate
147
+ @description = description
148
+ end
149
+
150
+ def parse(input, position = 0)
151
+ if position < input.length && @predicate.call(input[position])
152
+ ParseResult.success(input[position], input, position + 1)
153
+ else
154
+ ParseResult.failure("Expected #{@description}", input, position)
155
+ end
156
+ end
157
+ end
158
+
159
+ # Parse using regex
160
+ class Regex < Parser
161
+ def initialize(pattern, description = nil)
162
+ @pattern = pattern.is_a?(Regexp) ? pattern : Regexp.new("^#{pattern}")
163
+ @description = description || @pattern.inspect
164
+ end
165
+
166
+ def parse(input, position = 0)
167
+ remaining = input[position..]
168
+ match = @pattern.match(remaining)
169
+
170
+ if match && match.begin(0) == 0
171
+ matched = match[0]
172
+ ParseResult.success(matched, input, position + matched.length)
173
+ else
174
+ ParseResult.failure("Expected #{@description}", input, position)
175
+ end
176
+ end
177
+ end
178
+
179
+ # Parse end of input
180
+ class EndOfInput < Parser
181
+ def parse(input, position = 0)
182
+ if position >= input.length
183
+ ParseResult.success(nil, input, position)
184
+ else
185
+ ParseResult.failure("Expected end of input", input, position)
186
+ end
187
+ end
188
+ end
189
+
190
+ # Always succeed with a value
191
+ class Pure < Parser
192
+ def initialize(value)
193
+ @value = value
194
+ end
195
+
196
+ def parse(input, position = 0)
197
+ ParseResult.success(@value, input, position)
198
+ end
199
+ end
200
+
201
+ # Always fail
202
+ class Fail < Parser
203
+ def initialize(message)
204
+ @message = message
205
+ end
206
+
207
+ def parse(input, position = 0)
208
+ ParseResult.failure(@message, input, position)
209
+ end
210
+ end
211
+
212
+ # Lazy parser (for recursive grammars)
213
+ class Lazy < Parser
214
+ def initialize(&block)
215
+ @block = block
216
+ @parser = nil
217
+ end
218
+
219
+ def parse(input, position = 0)
220
+ @parser ||= @block.call
221
+ @parser.parse(input, position)
222
+ end
223
+ end
224
+
225
+ #==========================================================================
226
+ # Combinator Parsers
227
+ #==========================================================================
228
+
229
+ # Sequence two parsers
230
+ class Sequence < Parser
231
+ def initialize(left, right)
232
+ @left = left
233
+ @right = right
234
+ end
235
+
236
+ def parse(input, position = 0)
237
+ result1 = @left.parse(input, position)
238
+ return result1 if result1.failure?
239
+
240
+ result2 = @right.parse(input, result1.position)
241
+ return result2 if result2.failure?
242
+
243
+ ParseResult.success([result1.value, result2.value], input, result2.position)
244
+ end
245
+ end
246
+
247
+ # Alternative: try first, if fails try second
248
+ class Alternative < Parser
249
+ def initialize(left, right)
250
+ @left = left
251
+ @right = right
252
+ end
253
+
254
+ def parse(input, position = 0)
255
+ result = @left.parse(input, position)
256
+ return result if result.success?
257
+
258
+ @right.parse(input, position)
259
+ end
260
+ end
261
+
262
+ # Map result
263
+ class Map < Parser
264
+ def initialize(parser, func)
265
+ @parser = parser
266
+ @func = func
267
+ end
268
+
269
+ def parse(input, position = 0)
270
+ @parser.parse(input, position).map(&@func)
271
+ end
272
+ end
273
+
274
+ # FlatMap (bind)
275
+ class FlatMap < Parser
276
+ def initialize(parser, func)
277
+ @parser = parser
278
+ @func = func
279
+ end
280
+
281
+ def parse(input, position = 0)
282
+ result = @parser.parse(input, position)
283
+ return result if result.failure?
284
+
285
+ next_parser = @func.call(result.value)
286
+ next_parser.parse(input, result.position)
287
+ end
288
+ end
289
+
290
+ # Many: zero or more
291
+ class Many < Parser
292
+ def initialize(parser)
293
+ @parser = parser
294
+ end
295
+
296
+ def parse(input, position = 0)
297
+ results = []
298
+ current_pos = position
299
+
300
+ loop do
301
+ result = @parser.parse(input, current_pos)
302
+ break if result.failure?
303
+
304
+ results << result.value
305
+ break if result.position == current_pos # Prevent infinite loop
306
+ current_pos = result.position
307
+ end
308
+
309
+ ParseResult.success(results, input, current_pos)
310
+ end
311
+ end
312
+
313
+ # Many1: one or more
314
+ class Many1 < Parser
315
+ def initialize(parser)
316
+ @parser = parser
317
+ end
318
+
319
+ def parse(input, position = 0)
320
+ first = @parser.parse(input, position)
321
+ return first if first.failure?
322
+
323
+ results = [first.value]
324
+ current_pos = first.position
325
+
326
+ loop do
327
+ result = @parser.parse(input, current_pos)
328
+ break if result.failure?
329
+
330
+ results << result.value
331
+ break if result.position == current_pos
332
+ current_pos = result.position
333
+ end
334
+
335
+ ParseResult.success(results, input, current_pos)
336
+ end
337
+ end
338
+
339
+ # Optional: zero or one
340
+ class Optional < Parser
341
+ def initialize(parser)
342
+ @parser = parser
343
+ end
344
+
345
+ def parse(input, position = 0)
346
+ result = @parser.parse(input, position)
347
+ if result.success?
348
+ result
349
+ else
350
+ ParseResult.success(nil, input, position)
351
+ end
352
+ end
353
+ end
354
+
355
+ # Separated by delimiter
356
+ class SepBy < Parser
357
+ def initialize(parser, delimiter)
358
+ @parser = parser
359
+ @delimiter = delimiter
360
+ end
361
+
362
+ def parse(input, position = 0)
363
+ first = @parser.parse(input, position)
364
+ return ParseResult.success([], input, position) if first.failure?
365
+
366
+ results = [first.value]
367
+ current_pos = first.position
368
+
369
+ loop do
370
+ delim_result = @delimiter.parse(input, current_pos)
371
+ break if delim_result.failure?
372
+
373
+ item_result = @parser.parse(input, delim_result.position)
374
+ break if item_result.failure?
375
+
376
+ results << item_result.value
377
+ current_pos = item_result.position
378
+ end
379
+
380
+ ParseResult.success(results, input, current_pos)
381
+ end
382
+ end
383
+
384
+ # Separated by 1 (at least one)
385
+ class SepBy1 < Parser
386
+ def initialize(parser, delimiter)
387
+ @parser = parser
388
+ @delimiter = delimiter
389
+ end
390
+
391
+ def parse(input, position = 0)
392
+ first = @parser.parse(input, position)
393
+ return first if first.failure?
394
+
395
+ results = [first.value]
396
+ current_pos = first.position
397
+
398
+ loop do
399
+ delim_result = @delimiter.parse(input, current_pos)
400
+ break if delim_result.failure?
401
+
402
+ item_result = @parser.parse(input, delim_result.position)
403
+ break if item_result.failure?
404
+
405
+ results << item_result.value
406
+ current_pos = item_result.position
407
+ end
408
+
409
+ ParseResult.success(results, input, current_pos)
410
+ end
411
+ end
412
+
413
+ # Skip right: parse both, return left
414
+ class SkipRight < Parser
415
+ def initialize(left, right)
416
+ @left = left
417
+ @right = right
418
+ end
419
+
420
+ def parse(input, position = 0)
421
+ result1 = @left.parse(input, position)
422
+ return result1 if result1.failure?
423
+
424
+ result2 = @right.parse(input, result1.position)
425
+ return result2 if result2.failure?
426
+
427
+ ParseResult.success(result1.value, input, result2.position)
428
+ end
429
+ end
430
+
431
+ # Label for error messages
432
+ class Label < Parser
433
+ def initialize(parser, name)
434
+ @parser = parser
435
+ @name = name
436
+ end
437
+
438
+ def parse(input, position = 0)
439
+ result = @parser.parse(input, position)
440
+ if result.failure?
441
+ ParseResult.failure("Expected #{@name}", input, position)
442
+ else
443
+ result
444
+ end
445
+ end
446
+ end
447
+
448
+ # Lookahead: check without consuming
449
+ class Lookahead < Parser
450
+ def initialize(parser)
451
+ @parser = parser
452
+ end
453
+
454
+ def parse(input, position = 0)
455
+ result = @parser.parse(input, position)
456
+ if result.success?
457
+ ParseResult.success(result.value, input, position)
458
+ else
459
+ result
460
+ end
461
+ end
462
+ end
463
+
464
+ # Not followed by
465
+ class NotFollowedBy < Parser
466
+ def initialize(parser)
467
+ @parser = parser
468
+ end
469
+
470
+ def parse(input, position = 0)
471
+ result = @parser.parse(input, position)
472
+ if result.failure?
473
+ ParseResult.success(nil, input, position)
474
+ else
475
+ ParseResult.failure("Unexpected match", input, position)
476
+ end
477
+ end
478
+ end
479
+
480
+ # Choice: try multiple parsers in order
481
+ class Choice < Parser
482
+ def initialize(*parsers)
483
+ @parsers = parsers
484
+ end
485
+
486
+ def parse(input, position = 0)
487
+ best_error = nil
488
+ best_position = position
489
+
490
+ @parsers.each do |parser|
491
+ result = parser.parse(input, position)
492
+ return result if result.success?
493
+
494
+ if result.position >= best_position
495
+ best_error = result.error
496
+ best_position = result.position
497
+ end
498
+ end
499
+
500
+ ParseResult.failure(best_error || "No alternative matched", input, best_position)
501
+ end
502
+ end
503
+
504
+ # Chainl: left-associative chain
505
+ class ChainLeft < Parser
506
+ def initialize(term, op)
507
+ @term = term
508
+ @op = op
509
+ end
510
+
511
+ def parse(input, position = 0)
512
+ first = @term.parse(input, position)
513
+ return first if first.failure?
514
+
515
+ result = first.value
516
+ current_pos = first.position
517
+
518
+ loop do
519
+ op_result = @op.parse(input, current_pos)
520
+ break if op_result.failure?
521
+
522
+ term_result = @term.parse(input, op_result.position)
523
+ break if term_result.failure?
524
+
525
+ result = op_result.value.call(result, term_result.value)
526
+ current_pos = term_result.position
527
+ end
528
+
529
+ ParseResult.success(result, input, current_pos)
530
+ end
531
+ end
532
+
533
+ #==========================================================================
534
+ # DSL Module - Convenience methods
535
+ #==========================================================================
536
+
537
+ module DSL
538
+ def literal(str)
539
+ Literal.new(str)
540
+ end
541
+
542
+ def regex(pattern, description = nil)
543
+ Regex.new(pattern, description)
544
+ end
545
+
546
+ def satisfy(description = "character", &predicate)
547
+ Satisfy.new(predicate, description)
548
+ end
549
+
550
+ def char(c)
551
+ Literal.new(c)
552
+ end
553
+
554
+ def string(str)
555
+ Literal.new(str)
556
+ end
557
+
558
+ def eof
559
+ EndOfInput.new
560
+ end
561
+
562
+ def pure(value)
563
+ Pure.new(value)
564
+ end
565
+
566
+ def fail(message)
567
+ Fail.new(message)
568
+ end
569
+
570
+ def lazy(&block)
571
+ Lazy.new(&block)
572
+ end
573
+
574
+ def choice(*parsers)
575
+ Choice.new(*parsers)
576
+ end
577
+
578
+ def sequence(*parsers)
579
+ parsers.reduce { |acc, p| acc >> p }
580
+ end
581
+
582
+ # Common character parsers
583
+ def digit
584
+ satisfy("digit") { |c| c =~ /[0-9]/ }
585
+ end
586
+
587
+ def letter
588
+ satisfy("letter") { |c| c =~ /[a-zA-Z]/ }
589
+ end
590
+
591
+ def alphanumeric
592
+ satisfy("alphanumeric") { |c| c =~ /[a-zA-Z0-9]/ }
593
+ end
594
+
595
+ def whitespace
596
+ satisfy("whitespace") { |c| c =~ /\s/ }
597
+ end
598
+
599
+ def spaces
600
+ whitespace.many.map { |chars| chars.join }
601
+ end
602
+
603
+ def spaces1
604
+ whitespace.many1.map { |chars| chars.join }
605
+ end
606
+
607
+ def newline
608
+ char("\n") | string("\r\n")
609
+ end
610
+
611
+ def identifier
612
+ (letter >> (alphanumeric | char("_")).many).map do |(first, rest)|
613
+ first + rest.join
614
+ end
615
+ end
616
+
617
+ def integer
618
+ (char("-").optional >> digit.many1).map do |(sign, digits)|
619
+ num = digits.join.to_i
620
+ sign ? -num : num
621
+ end
622
+ end
623
+
624
+ def float
625
+ regex(/\-?\d+\.\d+/, "float").map(&:to_f)
626
+ end
627
+
628
+ def quoted_string(quote = '"')
629
+ content = satisfy("string character") { |c| c != quote && c != "\\" }
630
+ escape = (char("\\") >> satisfy("escape char")).map { |(_bs, c)| c }
631
+
632
+ (char(quote) >> (content | escape).many.map(&:join) << char(quote)).map { |(_, str)| str }
633
+ end
634
+
635
+ # Skip whitespace around parser
636
+ def lexeme(parser)
637
+ (spaces >> parser << spaces).map { |(_, val)| val }
638
+ end
639
+
640
+ # Chain for left-associative operators
641
+ def chainl(term, op)
642
+ ChainLeft.new(term, op)
643
+ end
644
+ end
645
+
646
+ #==========================================================================
647
+ # Type Parser - Parse T-Ruby type expressions
648
+ #==========================================================================
649
+
650
+ class TypeParser
651
+ include DSL
652
+
653
+ def initialize
654
+ build_parsers
655
+ end
656
+
657
+ def parse(input)
658
+ result = @type_expr.parse(input.strip)
659
+ if result.success?
660
+ { success: true, type: result.value, remaining: input[result.position..] }
661
+ else
662
+ { success: false, error: result.error, position: result.position }
663
+ end
664
+ end
665
+
666
+ private
667
+
668
+ def build_parsers
669
+ # Identifier (type name)
670
+ type_name = identifier.label("type name")
671
+
672
+ # Simple type
673
+ simple_type = type_name.map { |name| IR::SimpleType.new(name: name) }
674
+
675
+ # Lazy reference for recursive types
676
+ type_expr = lazy { @type_expr }
677
+
678
+ # Generic type arguments: <Type, Type, ...>
679
+ generic_args = (
680
+ lexeme(char("<")) >>
681
+ type_expr.sep_by1(lexeme(char(","))) <<
682
+ lexeme(char(">"))
683
+ ).map { |(_, types)| types }
684
+
685
+ # Generic type: Base<Args>
686
+ generic_type = (type_name >> generic_args.optional).map do |(name, args)|
687
+ if args && !args.empty?
688
+ IR::GenericType.new(base: name, type_args: args)
689
+ else
690
+ IR::SimpleType.new(name: name)
691
+ end
692
+ end
693
+
694
+ # Nullable type: Type?
695
+ nullable_suffix = char("?")
696
+
697
+ # Parenthesized type
698
+ paren_type = (lexeme(char("(")) >> type_expr << lexeme(char(")"))).map { |(_, t)| t }
699
+
700
+ # Function type: (Params) -> ReturnType
701
+ param_list = (
702
+ lexeme(char("(")) >>
703
+ type_expr.sep_by(lexeme(char(","))) <<
704
+ lexeme(char(")"))
705
+ ).map { |(_, params)| params }
706
+
707
+ arrow = lexeme(string("->"))
708
+
709
+ function_type = (param_list >> arrow >> type_expr).map do |((params, _arrow), ret)|
710
+ IR::FunctionType.new(param_types: params, return_type: ret)
711
+ end
712
+
713
+ # Tuple type: [Type, Type, ...]
714
+ tuple_type = (
715
+ lexeme(char("[")) >>
716
+ type_expr.sep_by1(lexeme(char(","))) <<
717
+ lexeme(char("]"))
718
+ ).map { |(_, types)| IR::TupleType.new(element_types: types) }
719
+
720
+ # Primary type (before operators)
721
+ primary_type = choice(
722
+ function_type,
723
+ tuple_type,
724
+ paren_type,
725
+ generic_type
726
+ )
727
+
728
+ # With optional nullable suffix
729
+ base_type = (primary_type >> nullable_suffix.optional).map do |(type, nullable)|
730
+ nullable ? IR::NullableType.new(inner_type: type) : type
731
+ end
732
+
733
+ # Union type: Type | Type | ...
734
+ union_op = lexeme(char("|"))
735
+ union_type = base_type.sep_by1(union_op).map do |types|
736
+ types.length == 1 ? types.first : IR::UnionType.new(types: types)
737
+ end
738
+
739
+ # Intersection type: Type & Type & ...
740
+ intersection_op = lexeme(char("&"))
741
+ @type_expr = union_type.sep_by1(intersection_op).map do |types|
742
+ types.length == 1 ? types.first : IR::IntersectionType.new(types: types)
743
+ end
744
+ end
745
+ end
746
+
747
+ #==========================================================================
748
+ # Declaration Parser - Parse T-Ruby declarations
749
+ #==========================================================================
750
+
751
+ class DeclarationParser
752
+ include DSL
753
+
754
+ def initialize
755
+ @type_parser = TypeParser.new
756
+ build_parsers
757
+ end
758
+
759
+ def parse(input)
760
+ result = @declaration.parse(input.strip)
761
+ if result.success?
762
+ { success: true, declarations: result.value }
763
+ else
764
+ { success: false, error: result.error, position: result.position }
765
+ end
766
+ end
767
+
768
+ def parse_file(input)
769
+ result = @program.parse(input)
770
+ if result.success?
771
+ { success: true, declarations: result.value.compact }
772
+ else
773
+ { success: false, error: result.error, position: result.position }
774
+ end
775
+ end
776
+
777
+ private
778
+
779
+ def build_parsers
780
+ # Type expression (delegate to TypeParser)
781
+ type_expr = lazy { parse_type_inline }
782
+
783
+ # Keywords
784
+ kw_type = lexeme(string("type"))
785
+ kw_interface = lexeme(string("interface"))
786
+ kw_def = lexeme(string("def"))
787
+ kw_end = lexeme(string("end"))
788
+ kw_class = lexeme(string("class"))
789
+ kw_module = lexeme(string("module"))
790
+
791
+ # Type alias: type Name = Definition
792
+ type_alias = (
793
+ kw_type >>
794
+ lexeme(identifier) <<
795
+ lexeme(char("=")) >>
796
+ regex(/[^\n]+/).map(&:strip)
797
+ ).map do |((_, name), definition)|
798
+ type_result = @type_parser.parse(definition)
799
+ if type_result[:success]
800
+ IR::TypeAlias.new(name: name, definition: type_result[:type])
801
+ else
802
+ nil
803
+ end
804
+ end
805
+
806
+ # Interface member: name: Type
807
+ interface_member = (
808
+ lexeme(identifier) <<
809
+ lexeme(char(":")) >>
810
+ regex(/[^\n]+/).map(&:strip)
811
+ ).map do |(name, type_str)|
812
+ type_result = @type_parser.parse(type_str)
813
+ if type_result[:success]
814
+ IR::InterfaceMember.new(name: name, type_signature: type_result[:type])
815
+ else
816
+ nil
817
+ end
818
+ end
819
+
820
+ # Interface: interface Name ... end
821
+ interface_body = (interface_member << (newline | spaces)).many
822
+
823
+ interface_decl = (
824
+ kw_interface >>
825
+ lexeme(identifier) <<
826
+ (newline | spaces) >>
827
+ interface_body <<
828
+ kw_end
829
+ ).map do |((_, name), members)|
830
+ IR::Interface.new(name: name, members: members.compact)
831
+ end
832
+
833
+ # Parameter: name: Type or name
834
+ param = (
835
+ identifier >>
836
+ (lexeme(char(":")) >> regex(/[^,)]+/).map(&:strip)).optional
837
+ ).map do |(name, type_str)|
838
+ type_node = if type_str
839
+ type_str_val = type_str.is_a?(Array) ? type_str.last : type_str
840
+ result = @type_parser.parse(type_str_val)
841
+ result[:success] ? result[:type] : nil
842
+ end
843
+ IR::Parameter.new(name: name, type_annotation: type_node)
844
+ end
845
+
846
+ # Parameters list
847
+ params_list = (
848
+ lexeme(char("(")) >>
849
+ param.sep_by(lexeme(char(","))) <<
850
+ lexeme(char(")"))
851
+ ).map { |(_, params)| params }
852
+
853
+ # Return type annotation
854
+ return_type = (
855
+ lexeme(char(":")) >>
856
+ regex(/[^\n]+/).map(&:strip)
857
+ ).map { |(_, type_str)| type_str }.optional
858
+
859
+ # Method definition: def name(params): ReturnType
860
+ method_def = (
861
+ kw_def >>
862
+ identifier >>
863
+ params_list.optional >>
864
+ return_type
865
+ ).map do |(((_, name), params), ret_str)|
866
+ ret_type = if ret_str
867
+ result = @type_parser.parse(ret_str)
868
+ result[:success] ? result[:type] : nil
869
+ end
870
+ IR::MethodDef.new(
871
+ name: name,
872
+ params: params || [],
873
+ return_type: ret_type
874
+ )
875
+ end
876
+
877
+ # Any declaration
878
+ @declaration = choice(
879
+ type_alias,
880
+ interface_decl,
881
+ method_def
882
+ )
883
+
884
+ # Line (declaration or empty)
885
+ line = (@declaration << (newline | eof)) | (spaces >> newline).map { nil }
886
+
887
+ # Program (multiple declarations)
888
+ @program = line.many
889
+ end
890
+
891
+ def parse_type_inline
892
+ Lazy.new { @type_parser.instance_variable_get(:@type_expr) }
893
+ end
894
+ end
895
+
896
+ #==========================================================================
897
+ # Error Reporting
898
+ #==========================================================================
899
+
900
+ class ParseError
901
+ attr_reader :message, :position, :line, :column, :input
902
+
903
+ def initialize(message:, position:, input:)
904
+ @message = message
905
+ @position = position
906
+ @input = input
907
+ @line, @column = calculate_line_column
908
+ end
909
+
910
+ def to_s
911
+ "Parse error at line #{@line}, column #{@column}: #{@message}"
912
+ end
913
+
914
+ def context(lines_before: 2, lines_after: 1)
915
+ input_lines = @input.split("\n")
916
+ start_line = [@line - lines_before - 1, 0].max
917
+ end_line = [@line + lines_after - 1, input_lines.length - 1].min
918
+
919
+ result = []
920
+ (start_line..end_line).each do |i|
921
+ prefix = i == @line - 1 ? ">>> " : " "
922
+ result << "#{prefix}#{i + 1}: #{input_lines[i]}"
923
+
924
+ if i == @line - 1
925
+ result << " " + " " * (@column + @line.to_s.length + 1) + "^"
926
+ end
927
+ end
928
+
929
+ result.join("\n")
930
+ end
931
+
932
+ private
933
+
934
+ def calculate_line_column
935
+ lines = @input[0...@position].split("\n", -1)
936
+ line = lines.length
937
+ column = lines.last&.length || 0
938
+ [line, column + 1]
939
+ end
940
+ end
941
+ end
942
+ end