Skip to content

Language Surface

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

Meris

Language Surface

package app.main
import app.shared.Message

pub value UserId(Raw: String)
alias UserName = String
data User(Id: UserId)
type LoadError = | Missing(Path: String) | Denied
trait Printable {
  fun Print(Value: Self): String
}

fun PrintUser(Value: User): String {
  Value.Id.Raw
}

impl Printable for User {
  Print = PrintUser
}

test "writes output" raises IoError {
  Fs.Write("out.txt", "ok")
}

entry fun Main(): Unit raises IoError {
  val CurrentUser = User(UserId("u-1"))
  Fs.Write("out.txt", CurrentUser.Id.Raw)
}

Script-mode files passed to meris check-script, meris run, or direct single-file meris compile may omit the explicit entry function and start with a top-level body:

val Text = Fs.Read("input.txt")!
Fs.Write("out.txt", Text)!

Status: parsed and type-checked. Interpreter support covers executable functions, script entry points, product and variant constructors, typed failures, handlers, value-level match, field access, control flow, and standard-library calls. JVM support includes script entry generation, value-level match, and compile-only java fun; the full backend matrix is listed on JVM Backend.

Parser diagnostics recover at declaration, block, call, and match-arm boundaries. A malformed function body should still allow later declarations in the same file to parse, so meris check can report the first syntax error without hiding the rest of the file's top-level shape.

Declarations

Top-level declarations may carry one visibility modifier:

pub value UserId(Raw: String)
internal fun Helper(): Unit {}
private type LocalState = | Ready
alias UserName = String
trait Printable {
  fun Print(Value: Self): String
}

fun PrintUserName(Value: UserName): String {
  Value
}

impl Printable for UserName {
  Print = PrintUserName
}

test "loads user" {
  val CurrentUser = User(UserId("u-1"), "Ada")
}

Declarations without a modifier are public for compatibility with existing Meris source. internal and private declarations are visible from the same package, and private declarations are limited to the same source file. See Declaration Visibility.

alias declares a transparent name for an existing type expression:

alias UserName = String
alias MaybeName = Option<String>
alias Maybe<T> = Option<T>

Aliases are not nominal. UserName checks as String, and MaybeName checks as Option<String>. Generic aliases such as Maybe<T> substitute their type arguments before checking. They do not create constructors, runtime declarations, or JVM classes. See Type Aliases.

trait declares checked contract metadata:

trait Printable {
  fun Print(Value: Self): String
}

fun PrintUserName(Value: UserName): String {
  Value
}

impl Printable for UserName {
  Print = PrintUserName
}

fun Main(Name: UserName): String {
  Printable.Print(Name)
}

fun MainWithSugar(Name: UserName): String {
  Name.Print()
}

Trait method signatures and impl conformance declarations are parsed, formatted, and type-checked. Self inside a trait method is substituted with the concrete target type while checking a conformance. Trait-qualified calls use Trait.Method(Value, ...); the first argument selects the conformance, and the checker rewrites the call to the mapped function. Instance-call sugar Value.Method(...) uses the same static conformance dispatch when exactly one visible conformance for the receiver type provides the method. The interpreter and JVM backend execute the mapped function call. See Traits.

value and data declare product shapes. The declaration also defines a constructor call with the same name and field order:

value UserId(Raw: String)
data User(Id: UserId, Name: String)

fun MakeUser(): User {
  User(UserId("u-1"), "Ada")
}

test declares a named executable check:

test "constructs user" {
  val CurrentUser = User(UserId("u-1"), "Ada")
}

Test bodies are checked as zero-argument Unit bodies. meris test executes them with the interpreter. Declared typed failures that escape the body are reported as failed tests. See Test Declarations.

class and config are not reserved. Meris does not currently define class or source-level config declarations. Use value or data for nominal product types, closed type variants for sum types, and trait plus impl for checked contracts. Project configuration belongs in meris.toml. Runtime configuration belongs behind explicit standard-library calls such as Env.Get and Env.Require.

Constructor calls are checked like declarations: argument count must match the field count, and each argument type must match the corresponding field type. Imported product constructors use the same import rules as type names. Field names in one value or data declaration must be unique.

type declares a closed variant set. Typed failures use the same variant declarations through raise, raises, and handle. Variants can be constructed as ordinary values with the qualified Type.Variant(...) form. Fieldless variants are already complete values and use Type.Variant without an empty argument list:

type BuildStatus = | Pending | Complete
type LoadError = | Missing(Path: String)

fun CurrentStatus(): BuildStatus {
  BuildStatus.Complete
}

fun MakeError(): LoadError {
  LoadError.Missing("config.meris")
}

Variant values are separate from throwable failures. BuildStatus.Complete and LoadError.Missing("config.meris") create values. raise LoadError.Missing creates a typed failure. Payload variants still require a constructor call, so LoadError.Missing without its field argument is rejected. Fieldless variants are not callable, so BuildStatus.Complete() is rejected. Payload field names in one variant must be unique.

fun declares a top-level function. entry fun marks the script entry point. Script validation requires exactly one entry function, no entry parameters, and a Unit return. Parameter names in one function must be unique.

java fun declares a compile-only static Java method boundary:

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

The declaration can be called like an ordinary Meris function in code compiled to JVM bytecode. Parameter names in one java fun must be unique. The interpreter does not execute it.

Expressions

Implemented expression forms include literals, names, blocks, local val bindings, function calls, product constructor calls, variant constructor calls, field access, arithmetic, boolean operations, if, raise, and handle. Local val bindings are immutable. value remains accepted for local bindings for compatibility, but the formatter emits val. Local names are unique in the current scope. A function body cannot declare a local binding with the same name as one of its parameters.

String literals are single-line and support \\, \", \n, \t, and \r escapes. Unknown escapes are lexer errors. The + operator supports Int + Int arithmetic and String + String concatenation. It does not convert other values to strings. == and != are defined for same-typed values whose type supports structural equality: Int, Bool, String, Unit, Option<T>, List<T>, products, and closed variants. Contained fields or elements must also support equality.

fun Choose(Ready: Bool): Int {
  if Ready { 1 } else { 0 }
}

match is parsed, type-checked for typed-variant exhaustiveness, and executable in the interpreter and JVM backend when the subject is an ordinary variant value. Use handle for typed failure recovery.

fun Explain(Error: LoadError): String {
  match Error {
    | Missing(Path) -> { Path }
    | Denied -> { "denied" }
  }
}

The selected arm receives field bindings from the matched variant. Arms that do not match are not evaluated. A match arm may include if Guard after the pattern. The guard is checked as Bool, runs after the pattern has matched and its fields are bound, and falls through to later arms when it is false. Guarded arms do not count toward static exhaustiveness; use an unguarded arm or wildcard for real coverage. Product values can also be matched by constructor-shaped product patterns:

match UserValue {
  | User(Name: DisplayName, ..) -> { DisplayName }
}

Named product fields use FieldName: Pattern. Omitting fields requires the explicit rest marker ..; mixed positional and named fields are rejected. raise passes payload arguments positionally when the raised variant declares fields. handle arms dispatch by failure type and variant name, then bind the carried payload fields by pattern name. Use Type.Variant patterns when composed errors share a variant name.

handle may also start with an ok arm. The ok arm binds the successful subject value and lets the handle expression map success and failure branches to a common result type:

handle Load() {
  ok Value -> { Value + 1 }
  | Missing -> { 0 }
}

Use _ for an explicit catch-all arm. It binds no fields and covers every variant not handled by an earlier arm:

fun Explain(Error: LoadError): String {
  match Error {
    | Missing(Path) -> { Path }
    | _ -> { "fallback" }
  }
}
type LoadError = | Missing(Path: String)

fun Load(): String raises LoadError {
  raise Missing("config.meris")
}

fun Main(): String {
  handle Load() {
    | LoadError.Missing(Path) -> { Path }
  }
}

raise LoadError.Missing(...) remains the explicit form. Fully qualified targets such as raise app.errors.LoadError.Missing(...) are accepted when the type is visible. When the enclosing function or test declares raises LoadError, Meris also accepts raise Missing(...). The shorthand is resolved only through the declaration's raised error set. If two declared errors expose the same variant name, the compiler requires the qualified form; the language will not guess.

Builtin standard errors such as JsonError and HttpError are different from source-declared variant errors because their catalogs are compiler-owned. They can be named in raises signatures, matched by known variant name, and bound by payload fields in handle arms. Wildcard arms still bind no payload.

Named raised-error set aliases can be used anywhere a raised-error set is written:

alias LoadFailure = raises LoadError | IoError

fun Load(): String raises LoadFailure {
  raise Missing("config.meris")
}

The checker expands LoadFailure before checking shorthand raise, unhandled errors, function type compatibility, lambda allowances, trait method compatibility, and handle exhaustiveness. Tooling keeps the alias spelling visible and shows the expanded concrete set in API reports, hover, signature help, and LSP diagnostic related information. The alias is source metadata, not a runtime object or JVM ABI surface.

Entry functions and tests may use postfix ! for fail-fast unwrap:

entry fun Main(): Unit {
  val Text = Fs.Read("input.txt")!
}

The expression keeps the success type of Fs.Read(...). If it fails, execution returns the same typed runtime failure that an unhandled raises path would return. Ordinary functions must handle the failure or declare it with raises.

Type Expressions

Generic builtin type expressions are parsed, formatted, and type-checked:

alias MaybeName = Option<String>
data MaybeNameBox(Value: MaybeName)

Option<Value> and List<Element> each require one type argument in ordinary Meris type expressions. The current JVM backend erases those arguments after checking. A generic type alias can declare type parameters and be used with matching type arguments; a non-generic alias cannot.

Modules

Files can declare one leading package and import explicit symbols after it.

package app.main
import app.shared.Message

The package declaration must be first when present. Imports must appear before declarations.

Standard library modules are not injected as globals. Use qualified names such as Args.Count, or import a specific function.

Related

Clone this wiki locally