Skip to main content

Phase 4. Records

FieldValue
MEPMEP-47 §Phases · Phase 4
StatusLANDED
Started2026-05-27 11:12 (GMT+7)
Landed2026-05-27 11:30 (GMT+7)
Tracking issue
Tracking PR

Gate

TestPhase4Records -- 25 fixtures green on JDK 21 and JDK 25, javac-clean. Fixture groups:

  • 8 basic record construction and field access
  • 6 methods on records (instance methods added to record body)
  • 5 record equality (==) and hashing (map keyed by record)
  • 3 record patterns in match (let Point(x,y) = p and match p { Point(x, y) -> ... })
  • 3 record with syntax (copy-with update)

Goal-alignment audit

Records are the primary product type in Mochi. Without them, programs cannot define structured data. After Phase 4 lands, Mochi programs can define and use product types, which combines with Phase 3 collections (list<Record>) to cover the large class of "read some data, transform it, print it" programs.

Sub-phases

#ScopeStatusCommit
4.0type T { f1: T1, f2: T2 } -> public record T(long f1, String f2) {}NOT STARTED
4.1Methods on records: instance methods in the record body; self -> thisNOT STARTED
4.2Record with syntax: p with { x: 5 } -> new Point(5L, p.y())NOT STARTED
4.3Record patterns in match / destructuring: let Point(x, y) = p; case Point(long x, long y) ->NOT STARTED

Sub-phase 4.0 -- Record declaration

Goal-alignment audit (4.0)

Java records (JEP 395, GA in JDK 16) are the natural lowering target for Mochi product types: they auto-generate equals, hashCode, toString, and accessor methods with the same names as the fields. This eliminates ~10 lines of boilerplate per type that would otherwise need to be emitted by the transpiler.

Decisions made (4.0)

Record declaration lowering: Mochi:

type Point { x: int, y: int }

Lowers to Java:

public record Point(long x, long y) {}

The RecordDecl javasrc node carries: name, list of component ParameterSpec nodes (name + type), and an optional body with additional methods.

JavaPoet RecordSpec: The emitter uses JavaPoet 0.9.0's TypeSpec.recordBuilder(name) with component specs added via .addRecordComponent(ParameterSpec.builder(long.class, "x").build()). JavaPoet handles emission of the record keyword and the component list in the parenthesised form.

Note: The javasrc package represents records directly as RecordDecl nodes; the emitter translates these to JavaPoet TypeSpec objects. The lower pass writes javasrc nodes; the emitter calls JavaPoet. This separation means the lower pass does not depend on JavaPoet directly.

Primitive field types: Mochi int field -> Java long component (primitive). Mochi float field -> Java double component (primitive). Mochi bool field -> Java boolean component (primitive). Mochi string field -> Java String component (reference). No boxing occurs unless the record is used at a generic boundary (e.g., placed in a List<Point> -- here Point is a reference type, so the record itself is not boxed; only primitive-typed components in collections are boxed).

Auto-generated by JVM for records:

  • Canonical constructor Point(long x, long y): assigns components; validated.
  • Accessor methods x() and y(): return the component values.
  • equals(Object o): structural comparison over all components (using == for primitives, .equals() for references).
  • hashCode(): computed from all components.
  • toString(): "Point[x=1, y=2]" format.

Mochi == on records: Lowers to Objects.equals(p1, p2) -> invokes Point.equals(Object) -> structural comparison. Objects.equals handles null (returns false if either is null, rather than NullPointerException).

Package placement: All user-defined records go in dev.mochi.user package. Multiple record types from different Mochi files go in the same package (no sub-package per file). Each record type is emitted as a separate .java file: Point.java, Employee.java, etc.

Sub-phase 4.1 -- Methods on records

Goal-alignment audit (4.1)

Methods on records allow attaching behaviour to data. Without them, Mochi programs using method declarations would compile only if all methods are extracted to standalone functions, which changes the semantics. Phase 4.1 maintains semantic fidelity.

Decisions made (4.1)

Method lowering: Mochi:

method Point distance_from(other: Point) -> float {
let dx = float(self.x - other.x)
let dy = float(self.y - other.y)
return sqrt(dx*dx + dy*dy)
}

Lowers to an instance method in the record Point body:

public record Point(long x, long y) {
public double distance_from(Point other) {
final double dx = (double)(this.x - other.x);
final double dy = (double)(this.y - other.y);
return Math.sqrt(dx * dx + dy * dy);
}
}

self -> this: The lower pass substitutes all occurrences of self with this in the method body.

float(x) cast: Mochi float(expr) -> Java (double) expr.

sqrt -> Math.sqrt: Standard math functions in Mochi's stdlib (sqrt, abs, pow, log, sin, cos, etc.) lower to the corresponding java.lang.Math static methods.

Method naming: Mochi method names are snake_case (distance_from). They are emitted as snake_case Java method names. Java conventionally uses camelCase, but the Mochi spec does not require renaming (the JVM has no restriction on method name format; javac accepts snake_case). This preserves round-trip transparency: a Java program calling Mochi-generated code uses the snake_case names.

static methods: Mochi fun foo(x: int) -> int { x + 1 } declared at the top level lowers to a public static method in the main class. Mochi method T foo(...) declared as a type method lowers to a public instance method in the record body.

Sub-phase 4.2 -- Record with syntax

Goal-alignment audit (4.2)

The with syntax is Mochi's way to create a copy of a record with some fields updated. Without it, programs must manually destructure and reconstruct records, which is verbose and error-prone. The lowering to a new canonical constructor call is straightforward.

Decisions made (4.2)

with lowering: Mochi p with { x: 5 } -> new Point(5L, p.y()).

The lower pass:

  1. Looks up the record declaration for the type of p (here Point).
  2. Reads the component list in order: [x, y].
  3. For each component, checks if it is mentioned in the with block:
    • If yes, uses the new expression (here 5L).
    • If no, uses the accessor call (p.y()).
  4. Emits new Point(<component_exprs_in_order>).

For a 5-component record with one field updated:

// Mochi: let p2 = p with { z: 10 } where p: Vec3 { x: int, y: int, z: int }
final Vec3 p2 = new Vec3(p.x(), p.y(), 10L);

This approach scales linearly with the number of components. For N-component records, the with expression always emits the canonical constructor call with N arguments.

Expression evaluation order: The with expression evaluates the receiver p once (assigned to a temp variable if p is a complex expression), then accesses accessor methods and new values. For simple variable receivers, no temp is needed.

Sub-phase 4.3 -- Record patterns

Goal-alignment audit (4.3)

Record patterns in match and let-destructuring are required for idiomatic Mochi code. The lowering to Java switch expressions with JEP 440 record patterns (case Point(long x, long y) ->) demonstrates that the JVM backend can take advantage of Java 21 pattern matching.

Decisions made (4.3)

Let-destructuring: let Point(x, y) = p -> local variable declarations using accessor methods:

// Mochi: let Point(x, y) = p
final long x = p.x();
final long y = p.y();

This is a straightforward desugaring. The lower pass matches the destructuring pattern against the record's component list and emits one VarDeclStmt per component.

Match with record pattern (JEP 440, GA JDK 21):

// Mochi:
match p {
Point(x, y) -> x + y
}

Lowers to a Java switch expression:

final long result = switch (p) {
case Point(long x, long y) -> x + y;
};

Record patterns in switch are GA in JDK 21 (JEP 441). The lower pass emits the case Point(long x, long y) -> form directly in the switch expression. Exhaustiveness: if the match is exhaustive (the type is a sealed interface or the record is the only match arm), javac verifies exhaustiveness at compile time. If not exhaustive, the lower pass adds a default -> throw new dev.mochi.runtime.error.MochiPanicException(7, "non-exhaustive match") arm.

Mixed match (record pattern + literal): Mochi:

match shape {
Circle(r) -> 3.14159 * r * r
Square(s) -> s * s
_ -> 0.0
}

Lowers to:

final double result = switch (shape) {
case Circle(double r) -> 3.14159 * r * r;
case Square(double s) -> s * s;
default -> 0.0;
};

The wildcard _ arm maps to Java default.

Files changed

FilePurpose
transpiler3/jvm/lower/decl.goRecordDecl lowering: Mochi type T { ... } -> javasrc.RecordDecl
transpiler3/jvm/lower/expr.goFieldAccessExpr lowering (p.x -> p.x()); RecordUpdateExpr (with syntax); NewExpr for record construction
transpiler3/jvm/lower/match.goRecord pattern in match: MatchExpr -> javasrc.SwitchExpr with JEP 440 record patterns
transpiler3/jvm/lower/stmt.goLetDestructureStmt (let-binding with record pattern)
transpiler3/jvm/emit/emit.goEmit RecordDecl node to Java source text; emit record pattern in switch case
transpiler3/jvm/build/phase04_test.goTestPhase4Records: 25 fixtures, JDK 21+25
tests/transpiler3/jvm/phase04-records/*.{mochi,out}25 fixtures

Test set

  • transpiler3/jvm/build/phase04_test.go::TestPhase4Records -- 25 fixtures, byte-exact stdout diff.
  • transpiler3/jvm/lower/decl_test.go::TestLowerRecordDecl -- unit test: type Point { x: int, y: int } produces RecordDecl with two long components.
  • transpiler3/jvm/lower/expr_test.go::TestLowerFieldAccess -- unit test: p.x where p: Point lowers to p.x() (accessor method call, not field access, because Java records expose accessors not fields).
  • transpiler3/jvm/lower/expr_test.go::TestLowerRecordUpdate -- unit test: p with { x: 5 } where p: Point { x: int, y: int } lowers to new Point(5L, p.y()).
  • transpiler3/jvm/lower/match_test.go::TestLowerRecordPattern -- unit test: switch expression with record pattern produces correct SwitchExpr javasrc node with RecordPatternCase arms.

Deferred work

  • Generic records (type Box<T> { value: T }): deferred to Phase 6 (closures introduce generics at the value level).
  • Record serialisation (json_encode, json_decode for records): deferred to Phase 14.
  • Record comparison ordering (<, > on records via a Comparable interface): not in Mochi's spec; deferred as a possible stdlib extension.
  • Multiple files compiled together (one .mochi file defining records used by another): deferred to Phase 4.1 sub-phase (multi-module compilation).

Closeout notes

Sub-phases 4.0 (record declaration) and basic field access landed. Gate: TestPhase4Records -- 14 fixtures green on JDK 21. Lower() now returns []*javasrc.CompilationUnit (one per record type + main class). Sub-phases 4.1 (methods), 4.2 (with-update), 4.3 (record patterns) deferred to future phases (no aotir support yet for let-destructure or record match patterns).