Skip to content

Commit e17b53c

Browse files
authored
feat: implement keyword arguments syntax (#27)
Add new syntax for keyword arguments that distinguishes from positional args: - `{ name: String }` → keyword args (destructuring) - `config: { host: String }` → Hash literal parameter - `**opts: Type` → double splat for forwarding Changes: - Extend IR Parameter class with :keyword kind and interface_ref - Add keyword args parsing in parser.rb - Add keyword args code generation in compiler.rb - Extract common string utilities to StringUtils module - Add comprehensive e2e tests (12 test cases) Closes #19
1 parent ecaa305 commit e17b53c

File tree

6 files changed

+647
-12
lines changed

6 files changed

+647
-12
lines changed

lib/t_ruby.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require_relative "t_ruby/config"
66

77
# Core infrastructure (must be loaded first)
8+
require_relative "t_ruby/string_utils"
89
require_relative "t_ruby/ir"
910
require_relative "t_ruby/parser_combinator"
1011
require_relative "t_ruby/smt_solver"

lib/t_ruby/compiler.rb

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,7 @@ def remove_param_types(params_str)
505505
params = []
506506
current = ""
507507
depth = 0
508+
brace_depth = 0
508509

509510
params_str.each_char do |char|
510511
case char
@@ -514,9 +515,16 @@ def remove_param_types(params_str)
514515
when ">", "]", ")"
515516
depth -= 1
516517
current += char
518+
when "{"
519+
brace_depth += 1
520+
current += char
521+
when "}"
522+
brace_depth -= 1
523+
current += char
517524
when ","
518-
if depth.zero?
519-
params << clean_param(current.strip)
525+
if depth.zero? && brace_depth.zero?
526+
cleaned = clean_param(current.strip)
527+
params.concat(Array(cleaned)) if cleaned
520528
current = ""
521529
else
522530
current += char
@@ -526,12 +534,39 @@ def remove_param_types(params_str)
526534
end
527535
end
528536

529-
params << clean_param(current.strip) unless current.empty?
537+
cleaned = clean_param(current.strip) unless current.empty?
538+
params.concat(Array(cleaned)) if cleaned
530539
params.join(", ")
531540
end
532541

533542
# Clean a single parameter (remove type annotation, preserve default value)
543+
# Returns String or Array of Strings (for keyword args group)
534544
def clean_param(param)
545+
param = param.strip
546+
return nil if param.empty?
547+
548+
# 1. 더블 스플랫: **name: Type -> **name
549+
if param.start_with?("**")
550+
match = param.match(/^\*\*(\w+)(?::\s*.+)?$/)
551+
return "**#{match[1]}" if match
552+
553+
return param
554+
end
555+
556+
# 2. 키워드 인자 그룹: { ... } 또는 { ... }: InterfaceName
557+
if param.start_with?("{")
558+
return clean_keyword_args_group(param)
559+
end
560+
561+
# 3. Hash 리터럴: name: { ... } -> name
562+
if param.match?(/^\w+:\s*\{/)
563+
match = param.match(/^(\w+):\s*\{.+\}(?::\s*\w+)?$/)
564+
return match[1] if match
565+
566+
return param
567+
end
568+
569+
# 4. 일반 파라미터: name: Type = value -> name = value 또는 name: Type -> name
535570
# Match: name: Type = value (with default value)
536571
if (match = param.match(/^(#{TRuby::IDENTIFIER_CHAR}+)\s*:\s*.+?\s*(=\s*.+)$/))
537572
"#{match[1]} #{match[2]}"
@@ -543,6 +578,69 @@ def clean_param(param)
543578
end
544579
end
545580

581+
# 키워드 인자 그룹을 Ruby 키워드 인자로 변환
582+
# { name: String, age: Integer = 0 } -> name:, age: 0
583+
# { name:, age: 0 }: UserParams -> name:, age: 0
584+
def clean_keyword_args_group(param)
585+
# { ... }: InterfaceName 또는 { ... } 형태 파싱
586+
interface_match = param.match(/^\{(.+)\}\s*:\s*\w+\s*$/)
587+
inline_match = param.match(/^\{(.+)\}\s*$/) unless interface_match
588+
589+
inner_content = if interface_match
590+
interface_match[1]
591+
elsif inline_match
592+
inline_match[1]
593+
else
594+
return param
595+
end
596+
597+
# 내부 파라미터 분리
598+
parts = split_nested_content(inner_content)
599+
keyword_params = []
600+
601+
parts.each do |part|
602+
part = part.strip
603+
next if part.empty?
604+
605+
if interface_match
606+
# interface 참조: name: default_value 또는 name:
607+
if (match = part.match(/^(\w+):\s*(.*)$/))
608+
name = match[1]
609+
default_value = match[2].strip
610+
keyword_params << if default_value.empty?
611+
"#{name}:"
612+
else
613+
"#{name}: #{default_value}"
614+
end
615+
end
616+
elsif (match = part.match(/^(\w+):\s*(.+)$/))
617+
# 인라인 타입: name: Type = default 또는 name: Type
618+
name = match[1]
619+
type_and_default = match[2].strip
620+
621+
# Type = default 분리
622+
default_value = extract_default_value(type_and_default)
623+
keyword_params << if default_value
624+
"#{name}: #{default_value}"
625+
else
626+
"#{name}:"
627+
end
628+
end
629+
end
630+
631+
keyword_params
632+
end
633+
634+
# 중첩된 내용을 콤마로 분리
635+
def split_nested_content(content)
636+
StringUtils.split_by_comma(content)
637+
end
638+
639+
# 타입과 기본값에서 기본값만 추출
640+
def extract_default_value(type_and_default)
641+
StringUtils.extract_default_value(type_and_default)
642+
end
643+
546644
# Erase return type annotations
547645
def erase_return_types(source)
548646
result = source.dup

lib/t_ruby/ir.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,15 +147,19 @@ def children
147147

148148
# Method parameter
149149
class Parameter < Node
150-
attr_accessor :name, :type_annotation, :default_value, :kind
150+
attr_accessor :name, :type_annotation, :default_value, :kind, :interface_ref
151151

152-
# kind: :required, :optional, :rest, :keyrest, :block
153-
def initialize(name:, type_annotation: nil, default_value: nil, kind: :required, **opts)
152+
# kind: :required, :optional, :rest, :keyrest, :block, :keyword
153+
# :keyword - 키워드 인자 (구조분해): { name: String } → def foo(name:)
154+
# :keyrest - 더블 스플랫: **opts: Type → def foo(**opts)
155+
# interface_ref - interface 참조 타입 (예: }: UserParams 부분)
156+
def initialize(name:, type_annotation: nil, default_value: nil, kind: :required, interface_ref: nil, **opts)
154157
super(**opts)
155158
@name = name
156159
@type_annotation = type_annotation
157160
@default_value = default_value
158161
@kind = kind
162+
@interface_ref = interface_ref
159163
end
160164
end
161165

lib/t_ruby/parser.rb

Lines changed: 181 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -244,18 +244,36 @@ def parse_parameters(params_str)
244244
param_list = split_params(params_str)
245245

246246
param_list.each do |param|
247-
param_info = parse_single_parameter(param)
248-
parameters << param_info if param_info
247+
param = param.strip
248+
249+
# 1. 더블 스플랫: **name: Type
250+
if param.start_with?("**")
251+
param_info = parse_double_splat_parameter(param)
252+
parameters << param_info if param_info
253+
# 2. 키워드 인자 그룹: { ... } 또는 { ... }: InterfaceName
254+
elsif param.start_with?("{")
255+
keyword_params = parse_keyword_args_group(param)
256+
parameters.concat(keyword_params) if keyword_params
257+
# 3. Hash 리터럴: name: { ... }
258+
elsif param.match?(/^\w+:\s*\{/)
259+
param_info = parse_hash_literal_parameter(param)
260+
parameters << param_info if param_info
261+
# 4. 일반 위치 인자: name: Type 또는 name: Type = default
262+
else
263+
param_info = parse_single_parameter(param)
264+
parameters << param_info if param_info
265+
end
249266
end
250267

251268
parameters
252269
end
253270

254271
def split_params(params_str)
255-
# Handle nested generics like Array<Map<String, Int>>
272+
# Handle nested generics, braces, brackets
256273
result = []
257274
current = ""
258275
depth = 0
276+
brace_depth = 0
259277

260278
params_str.each_char do |char|
261279
case char
@@ -265,8 +283,14 @@ def split_params(params_str)
265283
when ">", "]", ")"
266284
depth -= 1
267285
current += char
286+
when "{"
287+
brace_depth += 1
288+
current += char
289+
when "}"
290+
brace_depth -= 1
291+
current += char
268292
when ","
269-
if depth.zero?
293+
if depth.zero? && brace_depth.zero?
270294
result << current.strip
271295
current = ""
272296
else
@@ -281,8 +305,10 @@ def split_params(params_str)
281305
result
282306
end
283307

284-
def parse_single_parameter(param)
285-
match = param.match(/^(\w+)(?::\s*(.+?))?$/)
308+
# 더블 스플랫 파라미터 파싱: **opts: Type
309+
def parse_double_splat_parameter(param)
310+
# **name: Type
311+
match = param.match(/^\*\*(\w+)(?::\s*(.+?))?$/)
286312
return nil unless match
287313

288314
param_name = match[1]
@@ -291,6 +317,155 @@ def parse_single_parameter(param)
291317
result = {
292318
name: param_name,
293319
type: type_str,
320+
kind: :keyrest,
321+
}
322+
323+
if type_str
324+
type_result = @type_parser.parse(type_str)
325+
result[:ir_type] = type_result[:type] if type_result[:success]
326+
end
327+
328+
result
329+
end
330+
331+
# 키워드 인자 그룹 파싱: { name: String, age: Integer = 0 } 또는 { name:, age: 0 }: InterfaceName
332+
def parse_keyword_args_group(param)
333+
# { ... }: InterfaceName 형태 확인
334+
# 또는 { ... } 만 있는 형태 (인라인 타입)
335+
interface_match = param.match(/^\{(.+)\}\s*:\s*(\w+)\s*$/)
336+
inline_match = param.match(/^\{(.+)\}\s*$/) unless interface_match
337+
338+
if interface_match
339+
inner_content = interface_match[1]
340+
interface_name = interface_match[2]
341+
parse_keyword_args_with_interface(inner_content, interface_name)
342+
elsif inline_match
343+
inner_content = inline_match[1]
344+
parse_keyword_args_inline(inner_content)
345+
end
346+
end
347+
348+
# interface 참조 키워드 인자 파싱: { name:, age: 0 }: UserParams
349+
def parse_keyword_args_with_interface(inner_content, interface_name)
350+
parameters = []
351+
parts = split_keyword_args(inner_content)
352+
353+
parts.each do |part|
354+
part = part.strip
355+
next if part.empty?
356+
357+
# name: default_value 또는 name: 형태
358+
next unless part.match?(/^(\w+):\s*(.*)$/)
359+
360+
match = part.match(/^(\w+):\s*(.*)$/)
361+
param_name = match[1]
362+
default_value = match[2].strip
363+
default_value = nil if default_value.empty?
364+
365+
parameters << {
366+
name: param_name,
367+
type: nil, # interface에서 타입을 가져옴
368+
default_value: default_value,
369+
kind: :keyword,
370+
interface_ref: interface_name,
371+
}
372+
end
373+
374+
parameters
375+
end
376+
377+
# 인라인 타입 키워드 인자 파싱: { name: String, age: Integer = 0 }
378+
def parse_keyword_args_inline(inner_content)
379+
parameters = []
380+
parts = split_keyword_args(inner_content)
381+
382+
parts.each do |part|
383+
part = part.strip
384+
next if part.empty?
385+
386+
# name: Type = default 또는 name: Type 형태
387+
next unless part.match?(/^(\w+):\s*(.+)$/)
388+
389+
match = part.match(/^(\w+):\s*(.+)$/)
390+
param_name = match[1]
391+
type_and_default = match[2].strip
392+
393+
# Type = default 분리
394+
type_str, default_value = split_type_and_default(type_and_default)
395+
396+
result = {
397+
name: param_name,
398+
type: type_str,
399+
default_value: default_value,
400+
kind: :keyword,
401+
}
402+
403+
if type_str
404+
type_result = @type_parser.parse(type_str)
405+
result[:ir_type] = type_result[:type] if type_result[:success]
406+
end
407+
408+
parameters << result
409+
end
410+
411+
parameters
412+
end
413+
414+
# 키워드 인자 내부를 콤마로 분리 (중첩된 제네릭/배열/해시 고려)
415+
def split_keyword_args(content)
416+
StringUtils.split_by_comma(content)
417+
end
418+
419+
# 타입과 기본값 분리: "String = 0" -> ["String", "0"]
420+
def split_type_and_default(type_and_default)
421+
StringUtils.split_type_and_default(type_and_default)
422+
end
423+
424+
# Hash 리터럴 파라미터 파싱: config: { host: String, port: Integer }
425+
def parse_hash_literal_parameter(param)
426+
# name: { ... } 또는 name: { ... }: InterfaceName
427+
match = param.match(/^(\w+):\s*(\{.+\})(?::\s*(\w+))?$/)
428+
return nil unless match
429+
430+
param_name = match[1]
431+
hash_type = match[2]
432+
interface_name = match[3]
433+
434+
result = {
435+
name: param_name,
436+
type: interface_name || hash_type,
437+
kind: :required,
438+
hash_type_def: hash_type, # 원본 해시 타입 정의 저장
439+
}
440+
441+
result[:interface_ref] = interface_name if interface_name
442+
443+
result
444+
end
445+
446+
def parse_single_parameter(param)
447+
# name: Type = default 또는 name: Type 또는 name
448+
# 기본값이 있는 경우 먼저 처리
449+
type_str = nil
450+
default_value = nil
451+
452+
if param.include?(":")
453+
match = param.match(/^(\w+):\s*(.+)$/)
454+
return nil unless match
455+
456+
param_name = match[1]
457+
type_and_default = match[2].strip
458+
type_str, default_value = split_type_and_default(type_and_default)
459+
else
460+
# 타입 없이 이름만 있는 경우
461+
param_name = param.strip
462+
end
463+
464+
result = {
465+
name: param_name,
466+
type: type_str,
467+
default_value: default_value,
468+
kind: default_value ? :optional : :required,
294469
}
295470

296471
# Parse type with combinator

0 commit comments

Comments
 (0)