Elvelt

Frontends

Ruby Frontend

Ruby Frontend

The Ruby frontend is a direct subclass of plugin::ILanguageFrontend that implements its own line-based, indentation-sensitive parser. It is the most whitespace-significant language in MPL_Compiler — every block ends with end, every method definition begins with def, and the expression parser supports block literals { |x| x + 1 } and do |x| x + 1 end.

File Lines Purpose
frontends/ruby/RubyFrontend.h 1,928 Frontend class with full statement + expression parser, block literal detection, attr_accessor/attr_reader/attr_writer, mixins
frontends/ruby/RubyStdLib.h 58 findStdLibMapping for puts, print, p, gets, exit, abort, rand

Language registration

// RubyFrontend.h:19-20
std::string languageName() const override { return "ruby"; }
std::vector<std::string> fileExtensions() const override { return {".rb"}; }

Only .rb is recognised.


Parsing model

RubyFrontend::parse (RubyFrontend.h:22-33):

  1. Preprocess: split source into trimmed lines. Empty lines and comments (#…) are dropped.
  2. Initialise state: scopeStack_.push_back({}) for module-level scope.
  3. parseBlock(false) (line 1374): walks lines until end of input. Inner blocks pass stopOnEnd=true so they stop at the matching end.

State

// RubyFrontend.h:36-38
std::vector<std::string> lines_;
size_t index_ = 0;
std::vector<std::unordered_set<std::string>> scopeStack_;

Block-terminating keywords

parseBlock recognises (and stops on, when stopOnEnd=true):

Keyword Where
end line 1378
else line 1382
elsif … line 1382
when … line 1385
rescue / rescue E line 1385
ensure line 1385

Top-level statement kinds

parseBlock (line 1374-1928) handles:

Statement File:line IR
require '…' / load '…' 1389-1400 ImportNode(module=…)
module Name … end 1402-1426 ClassDeclNode(metadata["ruby_kind"]="module")
class Name … end 1428-1475 ClassDeclNode(metadata["superclass"]=…)
class Name < Base … end 1432 ClassDeclNode(metadata["superclass"]="Base")
case x; when …; …; else …; end 1477-1515 SwitchNode with cases + default
begin; …; rescue E; …; ensure …; end 1517-1548 TryCatchNode
if …; …; elsif …; …; else …; end 1550-1577 IfNode chain
for x in 1..n; …; end 1579-1668 ForNode (range special-cased)
for x in iter; …; end 1621-1666 ForNode with index-based iteration
while cond; …; end 1670-1678 WhileNode
`n.times do i … end`
`n.times { i … }`
until cond; …; end 1700+ WhileNode(UnaryOpNode("!", cond))
loop do … end 1710+ infinite WhileNode(true)
def name(args, **kwargs, &block); …; end 1336-1372 FunctionDeclNode
x = expr / x += expr 1820-1830 VariableDeclNode or AssignmentNode
puts … / print … / p … parsePrint (1173) StandardLibraryCallNode
attr_accessor :a, :b tryParseAttrDeclaration (1207) One getter + one setter per symbol
attr_reader :a same getter only
attr_writer :a same setter only
include Mod / prepend Mod / extend Mod tryParseMixinDeclaration (1284) Mixin marker collected into the enclosing class's metadata["mixins"]
Point = Struct.new(:x, :y) tryEmitStructClass (1300) ClassDeclNode(metadata["ruby_kind"]="struct", "ruby_struct_fields"]="x,y")
Data = Data.define(:x, :y) 1310 Same as Struct

String interpolation

tryParseStringInterpolation (RubyFrontend.h:1091-1171) parses double-quoted strings containing #{expr}:

name = "World"
greeting = "Hello, #{name}! You are #{age + 1} years old."

Lowers to a left-folded BinaryOpNode("+", …) chain of literal parts and expression results.

'single quoted' strings do not interpolate.


Block literals

tryParseTrailingBlockLiteral (line 185) handles the two forms:

Brace form: expr { |params| body }

[1, 2, 3].each { |x| puts x }

The expr portion (everything before {) is the receiver; the { |x| puts x } portion becomes a LambdaNode(parameters=[("auto","x")]) with body containing puts x. Combined into a MethodCallNode where the lambda is appended as the last argument.

do form: expr do |params| body end

File.open(path) do |f|
  f.each_line do |line|
    puts line
  end
end

Same lowering but the body spans multiple lines; parseBlock(true) is called to capture everything up to end.


Implicit block parameter

Every def method auto-inserts __ruby_block_param as the last parameter (line 1367-1369). This lets the body call yield and lets callers pass a block:

def twice
  yield 1
  yield 2
end

twice { |x| puts x * 2 }
# => 2, 4

Lowered to:

FunctionDeclNode(name="twice",
  parameters=[("auto","__ruby_block_param")],
  body: yield 1; yield 2)

Modules vs classes

module Name … end (line 1402) and class Name … end (line 1428) both produce ClassDeclNode. The ruby_kind metadata distinguishes them:

Construct IR metadata
module Foo metadata["ruby_kind"]="module"
class Foo (none)
class Foo < Bar metadata["superclass"]="Bar"
Point = Struct.new(...) metadata["ruby_kind"]="struct"
Data = Data.define(...) metadata["ruby_kind"]="struct", "ruby_struct_source"]="Data"
trait Foo (PHP — same shape) metadata["php_kind"]="trait" (PHP only)

initialize becomes cls->constructor (line 1463).


Mixins

include, prepend, extend inside a class body become marker VariableDeclNodes that extractMixinIntoClass (line 1272) folds into the enclosing class's metadata["mixins"]:

class Foo
  include Comparable
  prepend Enumerable
  extend Forwardable
end

Lowers to:

ClassDeclNode(name="Foo", metadata=["mixins"="include:Comparable,prepend:Enumerable,extend:Forwardable"])

attr_accessor / attr_reader / attr_writer

tryParseAttrDeclaration (line 1207) expands these into actual methods:

class Point
  attr_accessor :x, :y
  attr_reader :id
end

Lowers to:

ClassDeclNode(name="Point")
  methods:
    FunctionDeclNode(name="x",
      parameters=[("auto","__ruby_block_param")],
      body=ReturnNode(MemberAccessNode("self","x")))
    FunctionDeclNode(name="x=",
      parameters=[("auto","v"),("auto","__ruby_block_param")],
      body=AssignmentNode(MemberAccessNode("self","x"), "v"))
    FunctionDeclNode(name="y", …)
    FunctionDeclNode(name="y=", …)
    FunctionDeclNode(name="id", …)

Refinements

module StringExtensions
  refine String do
    def shout
      upcase + "!"
    end
  end
end

Currently refine blocks are parsed as ordinary class/module bodies. The refine keyword is not specially recognised — see limitations.md.


Procs / lambdas

add = ->(a, b) { a + b }       # lambda
sub = Proc.new { |a, b| a - b } # proc

Both are lowered to LambdaNode. The -> syntax is parsed as the expression parser walks the right side.


Operators and expressions

parseExpr (line 459) handles every Ruby's operator precedence levels:

Operator Notes
** right-associative
+@, -@, ~, ! unary
*, /, %
+, -
<<, >>
&
|, ^
>, >=, <, <=
<=> spaceship
==, !=, =~, !~
&&
||
.., ... range
? : ternary
defined?
not
or, and low precedence
if/unless modifier puts x if cond
=/+=/-=/etc. assignment
defined?, super, yield control-flow-as-expression
->(args) { body } lambda literal

yield

yield is parsed as YieldNode(value=…) (inherited from BraceFrontendBase). yield with arguments becomes a YieldNode with arguments[] populated.


Standard library mapping (RubyStdLib.h)

The complete table at frontends/ruby/RubyStdLib.h:38-47:

Ruby MPL module.function
puts(...) Console.WriteLine(...)
print(...) Console.Write(...)
p(...) Console.WriteLine(...) (with .inspect semantics approximated)
gets Console.ReadLine(...)
exit(code) Process.Exit(code)
abort(msg) Process.Exit(code) (msg printed before exit)
rand / rand(n) Math.Random()

IR examples

Example 1 — class with mixin and accessors

class Person
  include Comparable

  attr_accessor :name, :age

  def initialize(name, age)
    @name = name
    @age = age
  end

  def <=>(other)
    age <=> other.age
  end
end

Lowers to:

ClassDeclNode(name="Person",
  metadata=["mixins"="include:Comparable"])
  constructor:
    FunctionDeclNode(name="initialize", isConstructor=true,
      parameters=[("auto","name"),("auto","age"),("auto","__ruby_block_param")],
      body=… @name = name; @age = age …)
  methods:
    FunctionDeclNode(name="name", parameters=[("auto","__ruby_block_param")],
      body=ReturnNode(MemberAccessNode("self","@name")))
    FunctionDeclNode(name="name=", parameters=[("auto","v"),("auto","__ruby_block_param")],
      body=AssignmentNode(MemberAccessNode("self","@name"), "v"))
    FunctionDeclNode(name="age", …)
    FunctionDeclNode(name="age=", …)
    FunctionDeclNode(name="<=>", parameters=[("auto","other"),("auto","__ruby_block_param")],
      body=BinaryOpNode("<=>", MemberAccessNode("self","@age"), MemberAccessNode(other,"@age")))

Example 2 — block literal

[1, 2, 3].each do |x|
  puts x * 2
end

Lowers to:

MethodCallNode(
  object=ArrayLiteralNode([1, 2, 3]),
  methodName="each",
  arguments=[LambdaNode(
    parameters=[("auto","x")],
    body=StandardLibraryCallNode("Console","WriteLine", BinaryOpNode("*", x, 2)))])

Example 3 — string interpolation

greeting = "Hello, #{name}! You are #{age} years old."

Lowers to:

VariableDeclNode(name="greeting",
  initializer=BinaryOpNode("+",
    BinaryOpNode("+",
      BinaryOpNode("+",
        BinaryOpNode("+",
          "Hello, ",
          IdentifierNode("name")),
        "! You are "),
      IdentifierNode("age")),
    " years old."))

What is not supported (Ruby-specific gaps)

  • Refinements (refine String do … end): no refinement scoping.
  • Singleton methods (def obj.method; end): not recognised outside the class body.
  • Method missing / respond_to_missing?: not synthesised.
  • Struct.new(:x) accessors: auto-generated getters/setters may not match Ruby's []/[]= semantics.
  • Symbol vs string: :"foo" and ':foo' are stored as the same identifier; :foo literal is a LiteralNode(makeString("foo")).
  • Frozen string literals: frozen_string_literal: true magic comment is ignored.
  • Lazy enumerator (Enumerator::Lazy): not detected.
  • Pattern matching (case … in pattern; …): not yet supported.
  • Numbered parameters (_1, _2, …): not yet recognised.
  • Safe navigation (obj&.method): converted to obj != null ? obj.method() : nil only in obvious cases.