Skip to content

Java Interop

Chris Michael edited this page Jun 5, 2026 · 26 revisions

Meris

Java Interop

java fun Abs(Value: Int): Int = java.lang.Math.abs
java type JavaStringBuilder = java.lang.StringBuilder
java new NewBuilder(Capacity: Int): JavaStringBuilder owned nonnull =
  java.lang.StringBuilder
java method BuilderText(Builder: JavaStringBuilder borrowed): String nonnull =
  java.lang.StringBuilder.toString
java fun Separator(): String nonnull = java.lang.System.lineSeparator
type LoadError = | Failed(Message: Option<String>)
java fun Read(): String nullable
  throws java.io.IOException as LoadError.Failed(Message = message) =
  app.Reader.read
java fun ApplyTwice(Mapper: fun(Int): Int sam app.IntMapper, Value: Int): Int =
  app.Callbacks.applyTwice

Status: Interpreter No. JVM Yes. CLI run No. CLI compile Yes.

java fun declares a static Java method boundary that compiled Meris code can call through an ordinary Meris function name. java type, java new, and java method add the first opaque Java object-handle boundary for compiled Meris code. Function-value parameters may opt into a narrow Java SAM adapter with explicit sam <java.interface.BinaryName> metadata.

Examples

java fun Abs(Value: Int): Int = java.lang.Math.abs

fun Magnitude(Value: Int): Int {
  Abs(Value)
}

Semantics

The declaration shape is:

java fun Name(Parameters...): Return = java.binary.Owner.method
java fun Name(Parameters...): Return nullability = java.binary.Owner.method
java fun Name(...): Return throws Java.Exception as Error.Variant = target
java fun Name(...): Return throws
  Java.Exception as Error.Variant(Field = message) = target
java type JavaHandle = java.binary.Owner
java new Name(Parameters...): JavaHandle owned nonnull = java.binary.Owner
java method Name(Receiver: JavaHandle borrowed, Parameters...): Return =
  java.binary.Owner.method

The target uses a Java binary owner name with dots, followed by the method name. For example, java.lang.Math.abs targets owner java.lang.Math and method abs. Static methods declared on Java interfaces are valid when the owner is available to the compiler; java.util.List.of emits as an interface static method reference.

The owner and exact public static method must resolve on the compiler classpath. If the owner is missing, Meris reports M7016. If the owner exists but no static method matches the declared Meris parameters and Java return shape, Meris reports M7017.

For Java classes outside the Meris distribution, pass the compile-time classpath to the CLI:

meris check src/main.meris --classpath build/java-classes
meris compile src/main.meris --out build/meris-classes \
  --classpath build/java-classes

Classpath entries use the host platform path separator and may point to class directories or jar files. The same Java classes must be present on the runtime java -cp command that runs the generated Meris bytecode.

Project manifests can hold the same classpath:

[project]
source = "src"
out = "build/classes"
classpath = ["build/java-classes", "lib/helper.jar"]

Use meris check --project . or meris compile --project . when the project has meris.toml.

This first subset is intentionally small:

Position Supported types
Static parameters Int, Bool, String
SAM callback parameters fun(...) values marked with explicit sam metadata
Instance receiver visible java type handle marked borrowed
Constructor returns matching java type owned nonnull
Returns Int, Bool, Unit, String nonnull, String nullable, String platform unsafe, declared Java handles with nullability markers

Reference returns must mark the Java nullability boundary:

Marker Meaning Status
nonnull Java method is known not to return null. JVM executable for String.
nullable Java method may return null. JVM executable as Meris Option<String>.
platform unsafe Java method nullability is unknown. JVM executable as Meris Option<String>.

Unmarked Java reference returns are rejected. String nullable keeps the Java method descriptor as String and exposes Meris Option<String>: null becomes Option.none(), non-null becomes Option.some(value). String platform is rejected unless it includes unsafe; the exposed type is still Option<String>. unsafe is not a general modifier. Int unsafe, String nonnull unsafe, and String nullable unsafe are rejected before lowering.

Declared Java exceptions may translate to Meris typed-error variants:

type LoadError = | Failed

java fun Read(): String nonnull
  throws java.io.IOException as LoadError.Failed =
  app.Reader.read

Payload variants require explicit field mappings:

type LoadError = | Failed(Message: Option<String>)

java fun Read(): String nullable
  throws java.io.IOException as LoadError.Failed(Message = message) =
  app.Reader.read

The JVM backend catches only the declared Java exception class around the interop call. It rethrows JvmRuntimeError with the mapped error type and variant, so existing handle expressions continue to dispatch by variant name and bind payload fields.

The first supported payload source is message. It reads Throwable.getMessage() and exposes it as Option<String>, because Java permits a null exception message. Mapping message into a String field is a type error. A payload translation must map every target variant field; partial payloads are rejected before lowering.

Each Java exception class may appear once per java fun; duplicate mappings are rejected before lowering.

java type declares a nominal Meris handle for one Java binary owner. The handle is opaque. Meris code cannot select Java fields, compare by Java object identity, construct the owner directly, or call arbitrary Java methods. It can only pass the handle through declared interop functions.

java new exposes a public constructor. The return must be the matching handle type marked owned nonnull; nullable and platform-unsafe constructor returns are rejected. Constructor overload selection is exact against checked Meris parameter types.

java method exposes a public instance method as an ordinary Meris function. The receiver is the first parameter and must be a visible Java handle marked borrowed. Instance-call sugar such as Builder.toString() remains outside the Java interop boundary.

Java collection copy adapters are documented separately. They copy Java List or object-array returns into Meris-owned immutable snapshots. The copy adapter is valid only on List returns in Meris source; array backing is inferred from the Java method signature. List copy Option<Element> preserves nullable Java elements when Element is Int, Bool, or String. Nested List elements are copied recursively when their leaves are supported scalar or explicit Option scalar elements. List nullable copy Element and List platform unsafe copy Element expose Option<List<Element>> for Java collection returns; array-backed nullable collection returns are not supported yet.

Function Values As Java SAMs

Meris adapts function values to Java single-abstract-method interfaces only when the Java interop declaration marks the parameter explicitly:

java fun ApplyTwice(Mapper: fun(Int): Int sam app.IntMapper, Value: Int): Int =
  app.Callbacks.applyTwice

The Meris-facing parameter remains fun(Int): Int. Ordinary function-value semantics do not change; the JVM backend binds the current Meris runtime to the function-value method handle and wraps that handle with the declared Java interface before the Java call.

The adapter target must be a public Java interface with exactly one abstract method. The current executable subset accepts SAM method parameters only for primitive-safe Int and Bool; returns may use Int, Bool, Unit, or String. Reference callback parameters are rejected until Meris has explicit Java-to-Meris callback nullability syntax.

SAM-adapted function types must not declare raises. If a callback can raise a typed Meris error, handle it before crossing into Java because the Java interface has no typed-error channel.

The interpreter does not execute Java interop declarations. A script that declares Java interop and is passed to meris run fails during CLI validation with M7003. Compile the script with meris compile when it uses Java interop.

The conformance suite includes an object interop fixture that exercises the declared Java handle path end to end: java type, java new, borrowed java method, checked visibility, and JVM execution through the compile lane. That fixture proves the boundary shape without opening Java objects to arbitrary field access or reflection.

The SAM adapter tests cover parser metadata, type-checker diagnostics, and JVM execution through an explicit Java functional interface. Generic erased Java functional interfaces and callback-specific typed-error mapping remain staged.

JVM Emission

The JVM backend derives the method descriptor from checked Meris types and emits INVOKESTATIC against the declared Java owner and method name. java new emits NEW, DUP, and INVOKESPECIAL <init>. java method emits INVOKEVIRTUAL or INVOKEINTERFACE after evaluating the receiver and arguments. Target resolution happens before lowering, so bytecode emission does not guess at missing Java owners, constructors, or methods. If the owner resolves to an interface on the compiler classpath, the emitted call uses an interface method reference.

No JVM descriptor appears in source code or frontend type references.

Related

Clone this wiki locally