Skip to main content

05. Type mapping table

This note enumerates the closed translation table the bridge uses to map RBS types to Mochi types. An item whose entire public signature falls inside the table is translated; any item with a single out-of-table type is skipped with a SkipReport.

Scalar types

RBS typeMochi typeNotes
IntegerintMaps to Mochi's 64-bit int. Ruby integers are arbitrary-precision; values outside [-2^63, 2^63) panic the shim at runtime.
FloatfloatMaps to Mochi's 64-bit float (IEEE 754 double).
StringstringOwned UTF-8 copy across the bridge.
SymbolstringSee the Symbol special case section below.
boolboolThe RBS literal types true and false are also both mapped to bool.
trueboolLiteral singleton; widened to bool.
falseboolLiteral singleton; widened to bool.
nilnilMaps to Mochi's nil type. Used in return position or union forms.
void (return only)unitvoid in non-return position is refused (see refusal section).

Composite types

RBS typeMochi typeNotes
Array[T] where T in tablelist<T>Owned copy across the bridge.
Hash[String, V] where V in tablemap<string, V>String keys.
Hash[Symbol, V] where V in tablemap<string, V>Symbol keys converted to strings; the shim strips the leading : (Ruby :foo → Mochi "foo").
T?T?RBS shorthand for T | nil; the bridge recognises both forms.
T | nilT?Expanded optional form; normalised to T? in the emitted Mochi.
[A, B]tuple<A, B>RBS tuple type. Both elements must be in table.
[A, B, C]tuple<A, B, C>Triples and higher supported up to arity 12.
Integer | FloatfloatNumeric widening union; the only non-nil binary union the bridge accepts.
String | SymbolstringSymbol-string widening; accepted as a convenience for APIs that accept either.

Optional types

RBS has two syntactic forms for "nullable T":

# Form 1: shorthand
def fetch: (String key) -> Integer?

# Form 2: explicit union
def fetch: (String key) -> (Integer | nil)

The bridge normalises both to Mochi's int?. The canonical emitted declaration:

extern fun fetch(key: string): int? from ruby "MyClass#fetch"

Proc and lambda types

RBS ^(A) -> B translates to Mochi fun(A): B when both A and B are in table:

RBSMochi
^(Integer) -> Stringfun(int): string
^(String, Integer) -> boolfun(string, int): bool
^(Float, Float, Float) -> Floatfun(float, float, float): float

Procs are supported up to arity 5. Procs where any parameter or return type is untyped are refused. Procs returning void in non-callback position are refused (ambiguous semantics; use explicit nil return instead).

Higher-arity procs (arity > 5) are refused in v1. The SkipReport instructs the user to hand-author an extern fun override.

Class-to-record translation

A Ruby class whose public interface is entirely attr_reader methods with types inside the table translates to a Mochi record. The RBS signature drives the translation, not the Ruby implementation:

class Point
attr_reader x: Integer
attr_reader y: Integer
def initialize: (Integer x, Integer y) -> void
end

Becomes:

record Point {
x: int,
y: int,
}

The initialize constructor is exposed as:

extern fun point_new(x: int, y: int): Point from ruby "Point.new"

A class that has any method beyond attr_reader / attr_accessor on in-table types, or any attr_reader with an out-of-table type, is refused as a record. The entire class is skipped with a SkipReport citing the first offending item.

attr_writer-only and attr_accessor fields: attr_accessor :x is accepted if x's type is in table; the bridge emits a mutable record field.

DataClass (Ruby 3.2+)

Data.define(:x, :y) with RBS annotations translates to a Mochi record identically to an all-attr_reader class:

class Coord < Data
attr_reader x: Float
attr_reader y: Float
end

Becomes:

record Coord {
x: float,
y: float,
}

DataClass instances are immutable in Ruby; the bridge emits the Mochi record as immutable (no mut modifier).

Struct translation

Struct.new(:x, :y) with RBS annotations translates to a Mochi record if all fields are in table:

class Vec2 < Struct
attr_reader x: Float
attr_reader y: Float
end

Becomes:

record Vec2 {
x: float,
y: float,
}

A Struct with any field outside the table is refused entirely (the bridge does not emit partial records).

Symbol special case

RBS Symbol becomes Mochi string. The generated shim includes a comment explaining the mapping:

# Symbol bridge note:
# Ruby symbol equality :foo == :foo maps correctly to string equality "foo" == "foo".
# Ruby's Symbol#to_proc behaviour (:upcase.to_proc) is NOT bridged.
# If you need to_proc semantics, write an explicit lambda wrapper.

Specifically: :upcase.to_proc (which creates a proc that calls .upcase on its argument) has no Mochi analogue and is not generated. Users needing this pattern must hand-author a lambda shim on the Ruby side and declare an explicit fun binding.

Symbol hash keys (:foo => value) are converted to string keys by the bridge: "foo" => value. This conversion is transparent at the Mochi call site.

Refusal cases

The following types cause a SkipReport entry for any item containing them:

RBS typeReason
untypedNo static type information; the bridge cannot emit a typed Mochi binding.
topRuby's top type has no Mochi analogue.
botRuby's bottom type has no Mochi analogue.
selfOpen-class metaprogramming; the concrete type is not statically knowable.
instanceSame as self.
classSame as self.
A | B | C (3+ branch non-nil union)Complex union; the bridge only accepts T | nil and the two widening pairs above.
void (non-return position)void as a parameter or field type is semantically unclear in RBS.
IO / FilePlatform-dependent; not safe to bridge.
BasicObjectToo low-level; no RBS surface.
EncodingEncoding metadata object; no Mochi analogue.
FiberCoroutine handle; no Mochi analogue in v1.
ThreadConcurrency primitive; no Mochi analogue in v1.
Proc with untyped param or returnStatic type information missing.
Proc with arity > 5v1 arity limit.

SkipReport format

Each skipped item produces a SkipReport Go struct:

type SkipReport struct {
Gem string // e.g., "nokogiri"
Item string // e.g., "Nokogiri::XML::Document#xpath"
Reason SkipReason
RBSType string // the verbatim RBS type string that caused the skip
}

type SkipReason int
const (
SkipUntyped SkipReason = iota // untyped in signature
SkipTopBot // top or bot type
SkipSelfInstanceClass // self / instance / class
SkipComplexUnion // 3+ branch union beyond T|nil
SkipVoidNonReturn // void in non-return position
SkipIOFile // IO or File type
SkipBasicObject // BasicObject
SkipEncoding // Encoding
SkipFiber // Fiber
SkipThread // Thread
SkipProcUntyped // Proc with untyped A or B
SkipProcHighArity // Proc with arity > 5
SkipNoRBS // no RBS at all for this item
SkipStructPartial // Struct with out-of-table field
SkipClassPartial // class with out-of-table method
)

The SkipReport list is printed during mochi pkg lock and written to <workdir>/ruby_wrap/<gem>/skip_report.txt:

SKIPPED: nokogiri / Nokogiri::XML::Document#xpath
Reason: SkipUntyped
RBSType: untyped
Override: hand-author an extern fun binding in mochi.toml [[ruby.extern]]

SKIPPED: mysql2 / Mysql2::Client#query
Reason: SkipNoRBS
RBSType: (none)
Override: add RBS annotations or use [[ruby.extern]] to bind manually

Cross-references