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

type-ruby / t-ruby / 20560974963

28 Dec 2025 11:21PM UTC coverage: 79.076% (+1.7%) from 77.331%
20560974963

push

github

web-flow
refactor: migrate parser from regex to token-based parser combinator (#29)

* refactor: migrate parser from regex to token-based parser combinator

- Replace monolithic parser_combinator.rb (2833 lines) with modular architecture
- Add Scanner for tokenization with regex literal support
- Create IR::InterpolatedString for string interpolation parsing
- Fix type inference for interpolated strings (returns String)
- Add TRuby::ParseError for unified error handling
- Organize parsers into primitives/, combinators/, and token/ directories
- Each file contains exactly one class (snake_case filename matches PascalCase class)

* fix: enhance parser to support ternary, splat args, and statement expressions

- Add ternary operator (? :) parsing in ExpressionParser
- Support double splat (**opts) and single splat (*args) in method calls
- Support keyword arguments (name: value) in method calls
- Allow case/if/unless/begin as assignment right-hand side values
- Improve generic type compatibility (Array[untyped] with Array[T])

Fixes type inference errors in keyword_args samples.

* style: fix RuboCop violations and adjust metrics limits

* fix: require set for Ruby 3.1 compatibility

1849 of 2098 new or added lines in 53 files covered. (88.13%)

6 existing lines in 2 files now uncovered.

6644 of 8402 relevant lines covered (79.08%)

908.09 hits per line

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

74.43
/lib/t_ruby/ast_type_inferrer.rb
1
# frozen_string_literal: true
2

3
module TRuby
1✔
4
  # ASTTypeInferrer - TypeScript 스타일 정적 타입 추론 엔진
5
  # IR 노드를 순회하면서 타입을 추론하고 캐싱
6
  class ASTTypeInferrer
1✔
7
    # 리터럴 타입 매핑
8
    LITERAL_TYPE_MAP = {
1✔
9
      string: "String",
10
      integer: "Integer",
11
      float: "Float",
12
      boolean: "bool",
13
      symbol: "Symbol",
14
      nil: "nil",
15
      array: "Array[untyped]",
16
      hash: "Hash[untyped, untyped]",
17
    }.freeze
18

19
    # 산술 연산자 규칙 (피연산자 타입 → 결과 타입)
20
    ARITHMETIC_OPS = %w[+ - * / % **].freeze
1✔
21
    COMPARISON_OPS = %w[== != < > <= >= <=>].freeze
1✔
22
    LOGICAL_OPS = %w[&& ||].freeze
1✔
23

24
    # 내장 메서드 반환 타입
25
    BUILTIN_METHODS = {
26
      # String 메서드
27
      %w[String upcase] => "String",
1✔
28
      %w[String downcase] => "String",
29
      %w[String capitalize] => "String",
30
      %w[String reverse] => "String",
31
      %w[String strip] => "String",
32
      %w[String chomp] => "String",
33
      %w[String chop] => "String",
34
      %w[String gsub] => "String",
35
      %w[String sub] => "String",
36
      %w[String tr] => "String",
37
      %w[String to_s] => "String",
38
      %w[String to_str] => "String",
39
      %w[String to_sym] => "Symbol",
40
      %w[String to_i] => "Integer",
41
      %w[String to_f] => "Float",
42
      %w[String length] => "Integer",
43
      %w[String size] => "Integer",
44
      %w[String bytesize] => "Integer",
45
      %w[String empty?] => "bool",
46
      %w[String include?] => "bool",
47
      %w[String start_with?] => "bool",
48
      %w[String end_with?] => "bool",
49
      %w[String match?] => "bool",
50
      %w[String split] => "Array[String]",
51
      %w[String chars] => "Array[String]",
52
      %w[String bytes] => "Array[Integer]",
53
      %w[String lines] => "Array[String]",
54

55
      # Integer 메서드
56
      %w[Integer to_s] => "String",
57
      %w[Integer to_i] => "Integer",
58
      %w[Integer to_f] => "Float",
59
      %w[Integer abs] => "Integer",
60
      %w[Integer even?] => "bool",
61
      %w[Integer odd?] => "bool",
62
      %w[Integer zero?] => "bool",
63
      %w[Integer positive?] => "bool",
64
      %w[Integer negative?] => "bool",
65
      %w[Integer times] => "Integer",
66
      %w[Integer upto] => "Enumerator[Integer]",
67
      %w[Integer downto] => "Enumerator[Integer]",
68

69
      # Float 메서드
70
      %w[Float to_s] => "String",
71
      %w[Float to_i] => "Integer",
72
      %w[Float to_f] => "Float",
73
      %w[Float abs] => "Float",
74
      %w[Float ceil] => "Integer",
75
      %w[Float floor] => "Integer",
76
      %w[Float round] => "Integer",
77
      %w[Float truncate] => "Integer",
78
      %w[Float nan?] => "bool",
79
      %w[Float infinite?] => "Integer?",
80
      %w[Float finite?] => "bool",
81
      %w[Float zero?] => "bool",
82
      %w[Float positive?] => "bool",
83
      %w[Float negative?] => "bool",
84

85
      # Array 메서드
86
      %w[Array length] => "Integer",
87
      %w[Array size] => "Integer",
88
      %w[Array count] => "Integer",
89
      %w[Array empty?] => "bool",
90
      %w[Array any?] => "bool",
91
      %w[Array all?] => "bool",
92
      %w[Array none?] => "bool",
93
      %w[Array include?] => "bool",
94
      %w[Array reverse] => "Array[untyped]",
95
      %w[Array sort] => "Array[untyped]",
96
      %w[Array uniq] => "Array[untyped]",
97
      %w[Array compact] => "Array[untyped]",
98
      %w[Array flatten] => "Array[untyped]",
99
      %w[Array join] => "String",
100
      %w[Array to_s] => "String",
101
      %w[Array to_a] => "Array[untyped]",
102

103
      # Hash 메서드
104
      %w[Hash length] => "Integer",
105
      %w[Hash size] => "Integer",
106
      %w[Hash empty?] => "bool",
107
      %w[Hash key?] => "bool",
108
      %w[Hash has_key?] => "bool",
109
      %w[Hash value?] => "bool",
110
      %w[Hash has_value?] => "bool",
111
      %w[Hash include?] => "bool",
112
      %w[Hash keys] => "Array[untyped]",
113
      %w[Hash values] => "Array[untyped]",
114
      %w[Hash to_s] => "String",
115
      %w[Hash to_a] => "Array[untyped]",
116
      %w[Hash to_h] => "Hash[untyped, untyped]",
117

118
      # Object 메서드 (모든 타입에 적용)
119
      %w[Object to_s] => "String",
120
      %w[Object inspect] => "String",
121
      %w[Object class] => "Class",
122
      %w[Object is_a?] => "bool",
123
      %w[Object kind_of?] => "bool",
124
      %w[Object instance_of?] => "bool",
125
      %w[Object respond_to?] => "bool",
126
      %w[Object nil?] => "bool",
127
      %w[Object frozen?] => "bool",
128
      %w[Object dup] => "untyped",
129
      %w[Object clone] => "untyped",
130
      %w[Object freeze] => "self",
131
      %w[Object tap] => "self",
132
      %w[Object then] => "untyped",
133
      %w[Object yield_self] => "untyped",
134

135
      # Symbol 메서드
136
      %w[Symbol to_s] => "String",
137
      %w[Symbol to_sym] => "Symbol",
138
      %w[Symbol length] => "Integer",
139
      %w[Symbol size] => "Integer",
140
      %w[Symbol empty?] => "bool",
141
    }.freeze
142

143
    attr_reader :type_cache
1✔
144

145
    def initialize
1✔
146
      @type_cache = {} # 노드 → 타입 캐시 (TypeScript의 지연 평가)
228✔
147
    end
148

149
    # 표현식 타입 추론
150
    # @param node [IR::Node] IR 노드
151
    # @param env [TypeEnv] 타입 환경
152
    # @return [String, IR::TypeNode, nil] 추론된 타입
153
    def infer_expression(node, env)
1✔
154
      # 캐시 확인 (지연 평가)
155
      cache_key = node.object_id
159✔
156
      return @type_cache[cache_key] if @type_cache.key?(cache_key)
159✔
157

158
      type = case node
159✔
159
             when IR::Literal
160
               infer_literal(node)
63✔
161
             when IR::InterpolatedString
162
               "String" # Interpolated strings always produce String
6✔
163
             when IR::VariableRef
164
               infer_variable_ref(node, env)
42✔
165
             when IR::BinaryOp
166
               infer_binary_op(node, env)
20✔
167
             when IR::UnaryOp
168
               infer_unary_op(node, env)
×
169
             when IR::MethodCall
170
               infer_method_call(node, env)
17✔
171
             when IR::ArrayLiteral
172
               infer_array_literal(node, env)
3✔
173
             when IR::HashLiteral
174
               infer_hash_literal(node, env)
×
175
             when IR::Assignment
176
               infer_assignment(node, env)
6✔
177
             when IR::Conditional
178
               infer_conditional(node, env)
×
179
             when IR::Block
180
               infer_block(node, env)
2✔
181
             when IR::Return
182
               infer_return(node, env)
×
183
             when IR::RawCode
UNCOV
184
               "untyped"
×
185
             else
186
               "untyped"
×
187
             end
188

189
      @type_cache[cache_key] = type
159✔
190
      type
159✔
191
    end
192

193
    # 메서드 반환 타입 추론
194
    # @param method_node [IR::MethodDef] 메서드 정의 IR
195
    # @param class_env [TypeEnv, nil] 클래스 타입 환경
196
    # @return [String, IR::TypeNode, nil] 추론된 반환 타입
197
    def infer_method_return_type(method_node, class_env = nil)
1✔
198
      return nil unless method_node.body
77✔
199

200
      # 메서드 스코프 생성
201
      env = TypeEnv.new(class_env)
70✔
202

203
      # 파라미터 타입 등록
204
      method_node.params.each do |param|
70✔
205
        param_type = param.type_annotation&.to_rbs || "untyped"
50✔
206
        env.define(param.name, param_type)
50✔
207
      end
208

209
      # 본문에서 반환 타입 수집
210
      return_types, terminated = collect_return_types(method_node.body, env)
70✔
211

212
      # 암묵적 반환값 추론 (마지막 표현식) - 종료되지 않은 경우만
213
      unless terminated
70✔
214
        implicit_return = infer_implicit_return(method_node.body, env)
66✔
215
        return_types << implicit_return if implicit_return
66✔
216
      end
217

218
      # 타입 통합
219
      unify_types(return_types)
70✔
220
    end
221

222
    private
1✔
223

224
    # 리터럴 타입 추론
225
    def infer_literal(node)
1✔
226
      LITERAL_TYPE_MAP[node.literal_type] || "untyped"
63✔
227
    end
228

229
    # 변수 참조 타입 추론
230
    def infer_variable_ref(node, env)
1✔
231
      # 상수(클래스명)는 그 자체가 타입 (예: MyClass.new 호출 시)
232
      if node.scope == :constant || node.name.match?(/^[A-Z]/)
42✔
233
        return node.name
1✔
234
      end
235

236
      env.lookup(node.name) || "untyped"
41✔
237
    end
238

239
    # 이항 연산자 타입 추론
240
    def infer_binary_op(node, env)
1✔
241
      left_type = infer_expression(node.left, env)
20✔
242
      right_type = infer_expression(node.right, env)
20✔
243
      op = node.operator
20✔
244

245
      # 비교 연산자는 항상 bool
246
      return "bool" if COMPARISON_OPS.include?(op)
20✔
247

248
      # 논리 연산자
249
      if op == "&&"
19✔
250
        # && 는 falsy면 왼쪽, truthy면 오른쪽 반환
251
        return right_type # 단순화: 오른쪽 타입 반환
×
252
      end
253

254
      if op == "||"
19✔
255
        # || 는 truthy면 왼쪽, falsy면 오른쪽 반환
256
        return union_type(left_type, right_type)
×
257
      end
258

259
      # 산술 연산자
260
      if ARITHMETIC_OPS.include?(op)
19✔
261
        return infer_arithmetic_result(left_type, right_type, op)
3✔
262
      end
263

264
      "untyped"
16✔
265
    end
266

267
    # 산술 연산 결과 타입 추론
268
    def infer_arithmetic_result(left_type, right_type, op)
1✔
269
      left_base = base_type(left_type)
3✔
270
      right_base = base_type(right_type)
3✔
271

272
      # 문자열 연결
273
      if op == "+" && (left_base == "String" || right_base == "String")
3✔
274
        return "String"
1✔
275
      end
276

277
      # 숫자 연산
278
      if numeric_type?(left_base) && numeric_type?(right_base)
2✔
279
        # Float가 하나라도 있으면 Float
280
        return "Float" if left_base == "Float" || right_base == "Float"
2✔
281

282
        return "Integer"
1✔
283
      end
284

285
      # 배열 연결
UNCOV
286
      if op == "+" && left_base.start_with?("Array")
×
287
        return left_type
×
288
      end
289

UNCOV
290
      "untyped"
×
291
    end
292

293
    # 단항 연산자 타입 추론
294
    def infer_unary_op(node, env)
1✔
295
      operand_type = infer_expression(node.operand, env)
×
296

297
      case node.operator
×
298
      when "!"
299
        "bool"
×
300
      when "-"
301
        operand_type
×
302
      else
303
        "untyped"
×
304
      end
305
    end
306

307
    # 메서드 호출 타입 추론
308
    def infer_method_call(node, env)
1✔
309
      # receiver 타입 추론
310
      receiver_type = if node.receiver
17✔
311
                        infer_expression(node.receiver, env)
17✔
312
                      else
UNCOV
313
                        "Object"
×
314
                      end
315

316
      receiver_base = base_type(receiver_type)
17✔
317

318
      # 내장 메서드 조회
319
      method_key = [receiver_base, node.method_name]
17✔
320
      if BUILTIN_METHODS.key?(method_key)
17✔
321
        result = BUILTIN_METHODS[method_key]
10✔
322

323
        # self 반환인 경우 receiver 타입 반환
324
        return receiver_type if result == "self"
10✔
325

326
        return result
10✔
327
      end
328

329
      # Object 메서드 fallback
330
      object_key = ["Object", node.method_name]
7✔
331
      if BUILTIN_METHODS.key?(object_key)
7✔
332
        result = BUILTIN_METHODS[object_key]
×
333
        return receiver_type if result == "self"
×
334

335
        return result
×
336
      end
337

338
      # new 메서드는 클래스 인스턴스 반환
339
      if node.method_name == "new" && receiver_base.match?(/^[A-Z]/)
7✔
340
        return receiver_base
1✔
341
      end
342

343
      "untyped"
6✔
344
    end
345

346
    # 배열 리터럴 타입 추론
347
    def infer_array_literal(node, env)
1✔
348
      return "Array[untyped]" if node.elements.empty?
3✔
349

350
      element_types = node.elements.map { |e| infer_expression(e, env) }
7✔
351
      unified = unify_types(element_types)
2✔
352

353
      "Array[#{unified}]"
2✔
354
    end
355

356
    # 해시 리터럴 타입 추론
357
    def infer_hash_literal(node, env)
1✔
358
      return "Hash[untyped, untyped]" if node.pairs.empty?
×
359

360
      key_types = node.pairs.map { |p| infer_expression(p.key, env) }
×
361
      value_types = node.pairs.map { |p| infer_expression(p.value, env) }
×
362

363
      key_type = unify_types(key_types)
×
364
      value_type = unify_types(value_types)
×
365

366
      "Hash[#{key_type}, #{value_type}]"
×
367
    end
368

369
    # 대입 타입 추론 (변수 타입 업데이트 및 우변 타입 반환)
370
    def infer_assignment(node, env)
1✔
371
      value_type = infer_expression(node.value, env)
6✔
372

373
      # 변수 타입 등록
374
      target = node.target
6✔
375
      if target.start_with?("@") && !target.start_with?("@@")
6✔
376
        env.define_instance_var(target, value_type)
5✔
377
      elsif target.start_with?("@@")
1✔
378
        env.define_class_var(target, value_type)
×
379
      else
380
        env.define(target, value_type)
1✔
381
      end
382

383
      value_type
6✔
384
    end
385

386
    # 조건문 타입 추론 (then/else 브랜치 통합)
387
    def infer_conditional(node, env)
1✔
388
      then_type = infer_expression(node.then_branch, env) if node.then_branch
×
389
      else_type = infer_expression(node.else_branch, env) if node.else_branch
×
390

391
      types = [then_type, else_type].compact
×
392
      return "nil" if types.empty?
×
393

394
      unify_types(types)
×
395
    end
396

397
    # 블록 타입 추론 (마지막 문장의 타입)
398
    def infer_block(node, env)
1✔
399
      return "nil" if node.statements.empty?
2✔
400

401
      # 마지막 문장 타입 반환 (Ruby의 암묵적 반환)
402
      last_stmt = node.statements.last
1✔
403
      infer_expression(last_stmt, env)
1✔
404
    end
405

406
    # return 문 타입 추론
407
    def infer_return(node, env)
1✔
408
      return "nil" unless node.value
×
409

410
      infer_expression(node.value, env)
×
411
    end
412

413
    # 본문에서 모든 return 타입 수집
414
    # @return [Array<(Array<String>, Boolean)>] [수집된 타입들, 종료 여부]
415
    def collect_return_types(body, env)
1✔
416
      types = []
70✔
417

418
      terminated = collect_returns_recursive(body, env, types)
70✔
419

420
      [types, terminated]
70✔
421
    end
422

423
    # @return [Boolean] true if this node terminates (contains unconditional return)
424
    def collect_returns_recursive(node, env, types)
1✔
425
      case node
143✔
426
      when IR::Return
427
        type = node.value ? infer_expression(node.value, env) : "nil"
5✔
428
        types << type
5✔
429
        true # return은 항상 실행 흐름 종료
5✔
430
      when IR::Block
431
        node.statements.each do |stmt|
71✔
432
          terminated = collect_returns_recursive(stmt, env, types)
72✔
433
          return true if terminated # return 이후 코드는 unreachable
72✔
434
        end
435
        false
66✔
436
      when IR::Conditional
437
        then_terminated = node.then_branch ? collect_returns_recursive(node.then_branch, env, types) : false
1✔
438
        else_terminated = node.else_branch ? collect_returns_recursive(node.else_branch, env, types) : false
1✔
439
        # 모든 분기가 종료되어야 조건문 전체가 종료됨
440
        then_terminated && else_terminated
1✔
441
      else
442
        false
66✔
443
      end
444
    end
445

446
    # 암묵적 반환값 추론 (마지막 표현식)
447
    def infer_implicit_return(body, env)
1✔
448
      case body
66✔
449
      when IR::Block
450
        return nil if body.statements.empty?
66✔
451

452
        last_stmt = body.statements.last
62✔
453

454
        # return 문이면 이미 수집됨
455
        return nil if last_stmt.is_a?(IR::Return)
62✔
456

457
        infer_expression(last_stmt, env)
62✔
458
      else
459
        infer_expression(body, env)
×
460
      end
461
    end
462

463
    # 타입 통합 (여러 타입을 하나로)
464
    def unify_types(types)
1✔
465
      types = types.compact.uniq
72✔
466

467
      return "nil" if types.empty?
72✔
468
      return types.first if types.length == 1
68✔
469

470
      # nil과 다른 타입이 있으면 nullable
471
      if types.include?("nil") && types.length == 2
×
472
        other = types.find { |t| t != "nil" }
×
473
        return "#{other}?" if other
×
474
      end
475

476
      # 동일 기본 타입은 통합
477
      base_types = types.map { |t| base_type(t) }.uniq
×
478
      return types.first if base_types.length == 1
×
479

480
      # Union 타입 생성
481
      types.join(" | ")
×
482
    end
483

484
    # Union 타입 생성
485
    def union_type(type1, type2)
1✔
486
      return type2 if type1 == type2
×
487
      return type2 if type1 == "nil"
×
488
      return type1 if type2 == "nil"
×
489

490
      "#{type1} | #{type2}"
×
491
    end
492

493
    # 기본 타입 추출 (Generic에서)
494
    def base_type(type)
1✔
495
      return "untyped" if type.nil?
23✔
496

497
      type_str = type.is_a?(String) ? type : type.to_rbs
23✔
498

499
      # Array[X] → Array
500
      return ::Regexp.last_match(1) if type_str =~ /^(\w+)\[/
23✔
501

502
      # Nullable X? → X
503
      return type_str[0..-2] if type_str.end_with?("?")
17✔
504

505
      type_str
17✔
506
    end
507

508
    # 숫자 타입인지 확인
509
    def numeric_type?(type)
1✔
510
      %w[Integer Float Numeric].include?(type)
4✔
511
    end
512
  end
513
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

© 2026 Coveralls, Inc