-
-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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.
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.
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.
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.