• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

type-ruby / t-ruby / 20560723383

28 Dec 2025 10:58PM UTC coverage: 79.074% (+1.7%) from 77.331%
20560723383

Pull #29

github

web-flow
Merge 5876e651d into fda099366
Pull Request #29: refactor: migrate parser from regex to token-based parser combinator

1848 of 2097 new or added lines in 53 files covered. (88.13%)

6 existing lines in 2 files now uncovered.

6643 of 8401 relevant lines covered (79.07%)

908.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.92
/lib/t_ruby/parser_combinator/token/expression_parser.rb
1
# frozen_string_literal: true
2

3
module TRuby
1✔
4
  module ParserCombinator
1✔
5
    # Expression Parser - Parse expressions into IR nodes
6
    # Uses Pratt parser (operator precedence parsing) for correct precedence
7
    class ExpressionParser
1✔
8
      include TokenDSL
1✔
9

10
      # Operator precedence levels (higher = binds tighter)
11
      PRECEDENCE = {
1✔
12
        or_or: 1,      # ||
13
        and_and: 2,    # &&
14
        eq_eq: 3,      # ==
15
        bang_eq: 3,    # !=
16
        lt: 4,         # <
17
        gt: 4,         # >
18
        lt_eq: 4,      # <=
19
        gt_eq: 4,      # >=
20
        spaceship: 4,  # <=>
21
        pipe: 5,       # | (bitwise or)
22
        amp: 6,        # & (bitwise and)
23
        plus: 7,       # +
24
        minus: 7,      # -
25
        star: 8,       # *
26
        slash: 8,      # /
27
        percent: 8,    # %
28
        star_star: 9,  # ** (right-associative)
29
      }.freeze
30

31
      # Right-associative operators
32
      RIGHT_ASSOC = Set.new([:star_star]).freeze
1✔
33

34
      # Token type to operator symbol mapping
35
      OPERATOR_SYMBOLS = {
1✔
36
        or_or: :"||",
37
        and_and: :"&&",
38
        eq_eq: :==,
39
        bang_eq: :!=,
40
        lt: :<,
41
        gt: :>,
42
        lt_eq: :<=,
43
        gt_eq: :>=,
44
        spaceship: :<=>,
45
        plus: :+,
46
        minus: :-,
47
        star: :*,
48
        slash: :/,
49
        percent: :%,
50
        star_star: :**,
51
        pipe: :|,
52
        amp: :&,
53
      }.freeze
54

55
      def parse_expression(tokens, position = 0)
1✔
56
        parse_precedence(tokens, position, 0)
3,834✔
57
      end
58

59
      private
1✔
60

61
      def parse_precedence(tokens, position, min_precedence)
1✔
62
        result = parse_unary(tokens, position)
3,898✔
63
        return result if result.failure?
3,898✔
64

65
        left = result.value
3,883✔
66
        pos = result.position
3,883✔
67

68
        loop do
3,883✔
69
          break if pos >= tokens.length || tokens[pos].type == :eof
3,947✔
70

71
          operator_type = tokens[pos].type
228✔
72
          precedence = PRECEDENCE[operator_type]
228✔
73
          break unless precedence && precedence >= min_precedence
228✔
74

75
          pos += 1 # consume operator
64✔
76

77
          # Handle right associativity
78
          next_min = RIGHT_ASSOC.include?(operator_type) ? precedence : precedence + 1
64✔
79
          right_result = parse_precedence(tokens, pos, next_min)
64✔
80
          return right_result if right_result.failure?
64✔
81

82
          right = right_result.value
64✔
83
          pos = right_result.position
64✔
84

85
          left = IR::BinaryOp.new(
64✔
86
            operator: OPERATOR_SYMBOLS[operator_type],
87
            left: left,
88
            right: right
89
          )
90
        end
91

92
        # 삼항 연산자: condition ? then_branch : else_branch
93
        if pos < tokens.length && tokens[pos].type == :question
3,883✔
94
          pos += 1 # consume '?'
2✔
95

96
          then_result = parse_expression(tokens, pos)
2✔
97
          return then_result if then_result.failure?
2✔
98

99
          pos = then_result.position
2✔
100

101
          unless tokens[pos]&.type == :colon
2✔
NEW
102
            return TokenParseResult.failure("Expected ':' in ternary operator", tokens, pos)
×
103
          end
104

105
          pos += 1 # consume ':'
2✔
106

107
          else_result = parse_expression(tokens, pos)
2✔
108
          return else_result if else_result.failure?
2✔
109

110
          left = IR::Conditional.new(
2✔
111
            kind: :ternary,
112
            condition: left,
113
            then_branch: then_result.value,
114
            else_branch: else_result.value
115
          )
116
          pos = else_result.position
2✔
117
        end
118

119
        TokenParseResult.success(left, tokens, pos)
3,883✔
120
      end
121

122
      def parse_unary(tokens, position)
1✔
123
        return TokenParseResult.failure("End of input", tokens, position) if position >= tokens.length
3,900✔
124

125
        token = tokens[position]
3,900✔
126

127
        case token.type
3,900✔
128
        when :bang
129
          result = parse_unary(tokens, position + 1)
1✔
130
          return result if result.failure?
1✔
131

132
          node = IR::UnaryOp.new(operator: :!, operand: result.value)
1✔
133
          TokenParseResult.success(node, tokens, result.position)
1✔
134
        when :minus
135
          result = parse_unary(tokens, position + 1)
1✔
136
          return result if result.failure?
1✔
137

138
          # For negative number literals, we could fold them
139
          node = if result.value.is_a?(IR::Literal) && result.value.literal_type == :integer
1✔
140
                   IR::Literal.new(value: -result.value.value, literal_type: :integer)
1✔
NEW
141
                 elsif result.value.is_a?(IR::Literal) && result.value.literal_type == :float
×
NEW
142
                   IR::Literal.new(value: -result.value.value, literal_type: :float)
×
143
                 else
NEW
144
                   IR::UnaryOp.new(operator: :-, operand: result.value)
×
145
                 end
146
          TokenParseResult.success(node, tokens, result.position)
1✔
147
        else
148
          parse_postfix(tokens, position)
3,898✔
149
        end
150
      end
151

152
      def parse_postfix(tokens, position)
1✔
153
        result = parse_primary(tokens, position)
3,898✔
154
        return result if result.failure?
3,898✔
155

156
        left = result.value
3,887✔
157
        pos = result.position
3,887✔
158

159
        loop do
3,887✔
160
          break if pos >= tokens.length || tokens[pos].type == :eof
7,408✔
161

162
          case tokens[pos].type
3,740✔
163
          when :dot
164
            # Method call with receiver: obj.method or obj.method(args)
165
            pos += 1
3,507✔
166
            return TokenParseResult.failure("Expected method name after '.'", tokens, pos) if pos >= tokens.length
3,507✔
167

168
            method_token = tokens[pos]
3,507✔
169
            unless method_token.type == :identifier || keywords.key?(method_token.value)
3,507✔
NEW
170
              return TokenParseResult.failure("Expected method name", tokens, pos)
×
171
            end
172

173
            method_name = method_token.value
3,507✔
174
            pos += 1
3,507✔
175

176
            # Check for arguments
177
            args = []
3,507✔
178
            if pos < tokens.length && tokens[pos].type == :lparen
3,507✔
179
              args_result = parse_arguments(tokens, pos)
2✔
180
              return args_result if args_result.failure?
2✔
181

182
              args = args_result.value
2✔
183
              pos = args_result.position
2✔
184
            end
185

186
            left = IR::MethodCall.new(
3,507✔
187
              receiver: left,
188
              method_name: method_name,
189
              arguments: args
190
            )
191
          when :lbracket
192
            # Array access: arr[index]
193
            pos += 1
5✔
194
            index_result = parse_expression(tokens, pos)
5✔
195
            return index_result if index_result.failure?
5✔
196

197
            pos = index_result.position
5✔
198
            return TokenParseResult.failure("Expected ']'", tokens, pos) unless tokens[pos]&.type == :rbracket
5✔
199

200
            pos += 1
5✔
201

202
            left = IR::MethodCall.new(
5✔
203
              receiver: left,
204
              method_name: "[]",
205
              arguments: [index_result.value]
206
            )
207
          when :lparen
208
            # Function call without explicit receiver (left is identifier -> method call)
209
            break unless left.is_a?(IR::VariableRef) && left.scope == :local
13✔
210

211
            args_result = parse_arguments(tokens, pos)
13✔
212
            return args_result if args_result.failure?
13✔
213

214
            left = IR::MethodCall.new(
9✔
215
              method_name: left.name,
216
              arguments: args_result.value
217
            )
218
            pos = args_result.position
9✔
219

220
          else
221
            break
215✔
222
          end
223
        end
224

225
        TokenParseResult.success(left, tokens, pos)
3,883✔
226
      end
227

228
      def parse_primary(tokens, position)
1✔
229
        return TokenParseResult.failure("End of input", tokens, position) if position >= tokens.length
3,898✔
230

231
        token = tokens[position]
3,898✔
232

233
        case token.type
3,898✔
234
        when :integer
235
          node = IR::Literal.new(value: token.value.to_i, literal_type: :integer)
88✔
236
          TokenParseResult.success(node, tokens, position + 1)
88✔
237

238
        when :float
239
          node = IR::Literal.new(value: token.value.to_f, literal_type: :float)
2✔
240
          TokenParseResult.success(node, tokens, position + 1)
2✔
241

242
        when :string
243
          # Remove quotes from string value
244
          value = token.value[1..-2]
36✔
245
          node = IR::Literal.new(value: value, literal_type: :string)
36✔
246
          TokenParseResult.success(node, tokens, position + 1)
36✔
247

248
        when :string_start
249
          # Interpolated string: string_start, string_content*, string_end
250
          parse_interpolated_string(tokens, position)
16✔
251

252
        when :symbol
253
          # Remove : from symbol value
254
          value = token.value[1..].to_sym
8✔
255
          node = IR::Literal.new(value: value, literal_type: :symbol)
8✔
256
          TokenParseResult.success(node, tokens, position + 1)
8✔
257

258
        when true
259
          node = IR::Literal.new(value: true, literal_type: :boolean)
12✔
260
          TokenParseResult.success(node, tokens, position + 1)
12✔
261

262
        when false
263
          node = IR::Literal.new(value: false, literal_type: :boolean)
2✔
264
          TokenParseResult.success(node, tokens, position + 1)
2✔
265

266
        when :nil
267
          node = IR::Literal.new(value: nil, literal_type: :nil)
5✔
268
          TokenParseResult.success(node, tokens, position + 1)
5✔
269

270
        when :identifier
271
          node = IR::VariableRef.new(name: token.value, scope: :local)
3,693✔
272
          TokenParseResult.success(node, tokens, position + 1)
3,693✔
273

274
        when :constant
275
          node = IR::VariableRef.new(name: token.value, scope: :constant)
1✔
276
          TokenParseResult.success(node, tokens, position + 1)
1✔
277

278
        when :ivar
279
          node = IR::VariableRef.new(name: token.value, scope: :instance)
7✔
280
          TokenParseResult.success(node, tokens, position + 1)
7✔
281

282
        when :cvar
283
          node = IR::VariableRef.new(name: token.value, scope: :class)
1✔
284
          TokenParseResult.success(node, tokens, position + 1)
1✔
285

286
        when :gvar
287
          node = IR::VariableRef.new(name: token.value, scope: :global)
1✔
288
          TokenParseResult.success(node, tokens, position + 1)
1✔
289

290
        when :lparen
291
          # Parenthesized expression
292
          result = parse_expression(tokens, position + 1)
2✔
293
          return result if result.failure?
2✔
294

295
          pos = result.position
2✔
296
          return TokenParseResult.failure("Expected ')'", tokens, pos) unless tokens[pos]&.type == :rparen
2✔
297

298
          TokenParseResult.success(result.value, tokens, pos + 1)
2✔
299

300
        when :lbracket
301
          # Array literal
302
          parse_array_literal(tokens, position)
7✔
303

304
        when :lbrace
305
          # Hash literal
306
          parse_hash_literal(tokens, position)
13✔
307

308
        else
309
          TokenParseResult.failure("Unexpected token: #{token.type}", tokens, position)
4✔
310
        end
311
      end
312

313
      def parse_arguments(tokens, position)
1✔
314
        return TokenParseResult.failure("Expected '('", tokens, position) unless tokens[position]&.type == :lparen
15✔
315

316
        position += 1
15✔
317

318
        args = []
15✔
319

320
        # Empty arguments
321
        if tokens[position]&.type == :rparen
15✔
322
          return TokenParseResult.success(args, tokens, position + 1)
2✔
323
        end
324

325
        # Parse first argument
326
        result = parse_argument(tokens, position)
13✔
327
        return result if result.failure?
13✔
328

329
        args << result.value
9✔
330
        position = result.position
9✔
331

332
        # Parse remaining arguments
333
        while tokens[position]&.type == :comma
9✔
334
          position += 1
4✔
335
          result = parse_argument(tokens, position)
4✔
336
          return result if result.failure?
4✔
337

338
          args << result.value
4✔
339
          position = result.position
4✔
340
        end
341

342
        return TokenParseResult.failure("Expected ')'", tokens, position) unless tokens[position]&.type == :rparen
9✔
343

344
        TokenParseResult.success(args, tokens, position + 1)
9✔
345
      end
346

347
      # Parse a single argument (handles splat, double splat, and keyword arguments)
348
      def parse_argument(tokens, position)
1✔
349
        # Double splat argument: **expr
350
        if tokens[position]&.type == :star_star
17✔
NEW
351
          position += 1
×
NEW
352
          expr_result = parse_expression(tokens, position)
×
NEW
353
          return expr_result if expr_result.failure?
×
354

355
          # Wrap in a splat node (we'll use MethodCall with special name for now)
NEW
356
          node = IR::MethodCall.new(
×
357
            method_name: "**",
358
            arguments: [expr_result.value]
359
          )
NEW
360
          return TokenParseResult.success(node, tokens, expr_result.position)
×
361
        end
362

363
        # Single splat argument: *expr
364
        if tokens[position]&.type == :star
17✔
NEW
365
          position += 1
×
NEW
366
          expr_result = parse_expression(tokens, position)
×
NEW
367
          return expr_result if expr_result.failure?
×
368

NEW
369
          node = IR::MethodCall.new(
×
370
            method_name: "*",
371
            arguments: [expr_result.value]
372
          )
NEW
373
          return TokenParseResult.success(node, tokens, expr_result.position)
×
374
        end
375

376
        # Keyword argument: name: value
377
        if tokens[position]&.type == :identifier && tokens[position + 1]&.type == :colon
17✔
NEW
378
          key_name = tokens[position].value
×
NEW
379
          position += 2 # skip identifier and colon
×
380

NEW
381
          value_result = parse_expression(tokens, position)
×
NEW
382
          return value_result if value_result.failure?
×
383

384
          # Create a hash pair for keyword argument
NEW
385
          key = IR::Literal.new(value: key_name.to_sym, literal_type: :symbol)
×
NEW
386
          node = IR::HashPair.new(key: key, value: value_result.value)
×
NEW
387
          return TokenParseResult.success(node, tokens, value_result.position)
×
388
        end
389

390
        # Regular expression argument
391
        parse_expression(tokens, position)
17✔
392
      end
393

394
      def parse_array_literal(tokens, position)
1✔
395
        return TokenParseResult.failure("Expected '['", tokens, position) unless tokens[position]&.type == :lbracket
7✔
396

397
        position += 1
7✔
398

399
        elements = []
7✔
400

401
        # Empty array
402
        if tokens[position]&.type == :rbracket
7✔
403
          node = IR::ArrayLiteral.new(elements: elements)
3✔
404
          return TokenParseResult.success(node, tokens, position + 1)
3✔
405
        end
406

407
        # Parse first element
408
        result = parse_expression(tokens, position)
4✔
409
        return result if result.failure?
4✔
410

411
        elements << result.value
4✔
412
        position = result.position
4✔
413

414
        # Parse remaining elements
415
        while tokens[position]&.type == :comma
4✔
416
          position += 1
7✔
417
          result = parse_expression(tokens, position)
7✔
418
          return result if result.failure?
7✔
419

420
          elements << result.value
7✔
421
          position = result.position
7✔
422
        end
423

424
        return TokenParseResult.failure("Expected ']'", tokens, position) unless tokens[position]&.type == :rbracket
4✔
425

426
        node = IR::ArrayLiteral.new(elements: elements)
4✔
427
        TokenParseResult.success(node, tokens, position + 1)
4✔
428
      end
429

430
      def parse_hash_literal(tokens, position)
1✔
431
        return TokenParseResult.failure("Expected '{'", tokens, position) unless tokens[position]&.type == :lbrace
13✔
432

433
        position += 1
13✔
434

435
        pairs = []
13✔
436

437
        # Empty hash
438
        if tokens[position]&.type == :rbrace
13✔
439
          node = IR::HashLiteral.new(pairs: pairs)
3✔
440
          return TokenParseResult.success(node, tokens, position + 1)
3✔
441
        end
442

443
        # Parse first pair
444
        pair_result = parse_hash_pair(tokens, position)
10✔
445
        return pair_result if pair_result.failure?
10✔
446

447
        pairs << pair_result.value
3✔
448
        position = pair_result.position
3✔
449

450
        # Parse remaining pairs
451
        while tokens[position]&.type == :comma
3✔
452
          position += 1
5✔
453
          pair_result = parse_hash_pair(tokens, position)
5✔
454
          return pair_result if pair_result.failure?
5✔
455

456
          pairs << pair_result.value
5✔
457
          position = pair_result.position
5✔
458
        end
459

460
        return TokenParseResult.failure("Expected '}'", tokens, position) unless tokens[position]&.type == :rbrace
3✔
461

462
        node = IR::HashLiteral.new(pairs: pairs)
3✔
463
        TokenParseResult.success(node, tokens, position + 1)
3✔
464
      end
465

466
      def parse_hash_pair(tokens, position)
1✔
467
        # Handle symbol key shorthand: key: value
468
        if tokens[position]&.type == :identifier && tokens[position + 1]&.type == :colon
15✔
469
          key = IR::Literal.new(value: tokens[position].value.to_sym, literal_type: :symbol)
8✔
470
          position += 2 # skip identifier and colon
8✔
471
        else
472
          # Parse key expression
473
          key_result = parse_expression(tokens, position)
7✔
474
          return key_result if key_result.failure?
7✔
475

476
          key = key_result.value
7✔
477
          position = key_result.position
7✔
478

479
          # Expect => or :
480
          return TokenParseResult.failure("Expected ':' or '=>' in hash pair", tokens, position) unless tokens[position]&.type == :colon
7✔
481

NEW
482
          position += 1
×
483

484
        end
485

486
        # Parse value expression
487
        value_result = parse_expression(tokens, position)
8✔
488
        return value_result if value_result.failure?
8✔
489

490
        pair = IR::HashPair.new(key: key, value: value_result.value)
8✔
491
        TokenParseResult.success(pair, tokens, value_result.position)
8✔
492
      end
493

494
      def parse_interpolated_string(tokens, position)
1✔
495
        # string_start token contains the opening quote
496
        position += 1
16✔
497

498
        parts = []
16✔
499

500
        while position < tokens.length
16✔
501
          token = tokens[position]
64✔
502

503
          case token.type
64✔
504
          when :string_content
505
            parts << IR::Literal.new(value: token.value, literal_type: :string)
25✔
506
            position += 1
25✔
507
          when :interpolation_start
508
            # Skip #{ and parse expression
509
            position += 1
23✔
510
            expr_result = parse_expression(tokens, position)
23✔
511
            return expr_result if expr_result.failure?
23✔
512

513
            parts << expr_result.value
23✔
514
            position = expr_result.position
23✔
515

516
            # Expect interpolation_end (})
517
            return TokenParseResult.failure("Expected '}'", tokens, position) unless tokens[position]&.type == :interpolation_end
23✔
518

519
            position += 1
23✔
520

521
          when :string_end
522
            position += 1
16✔
523
            break
16✔
524
          else
NEW
525
            return TokenParseResult.failure("Unexpected token in string: #{token.type}", tokens, position)
×
526
          end
527
        end
528

529
        # Create interpolated string node
530
        node = IR::InterpolatedString.new(parts: parts)
16✔
531
        TokenParseResult.success(node, tokens, position)
16✔
532
      end
533

534
      def keywords
1✔
NEW
535
        @keywords ||= TRuby::Scanner::KEYWORDS
×
536
      end
537
    end
538
  end
539
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc