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):
- Preprocess: split source into trimmed lines. Empty lines and
comments (
#…) are dropped. - Initialise state:
scopeStack_.push_back({})for module-level scope. parseBlock(false)(line 1374): walks lines until end of input. Inner blocks passstopOnEnd=trueso they stop at the matchingend.
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;:fooliteral is aLiteralNode(makeString("foo")). - Frozen string literals:
frozen_string_literal: truemagic 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 toobj != null ? obj.method() : nilonly in obvious cases.