Expr.kt
src/jvmMain/kotlin/afield/lang/fieldml/ast/Expr.kt · 286 lines
package afield.lang.fieldml.ast
import afield.lang.fieldml.diag.SourceRange
FieldML's surface syntax tree: one node per piece of written syntax, each carrying the source range it was parsed from.
Parser produces this tree and Inferencer reads it. Nothing in it has a type yet. Every node
here has a counterpart in afield.lang.fieldml.type that carries an inferred type, and that typed tree is
lowered again into the afield.lang.fieldml.core form the evaluator runs.
The tree is narrower than the syntax in two places. ECall takes a single argument, so
application is already curried when it is parsed: add 20 22 is ECall(ECall(add, 20), 22). The
pipe operator builds that same node with its left operand as the argument, so 10 |> add 5 and
add 5 10 differ only in the ranges they record. And a constructor's argument types are bare
names: TypeRef has one case, TypeRefName, so a run of names in type Tree a = Node Tree Tree
reads as a run of separate arguments. Inference resolves each name to a built-in type, to the type
being declared, to one of that type's own parameters, or to a data type with no arguments.
It is wider than the syntax in one place: ELet and ELambda keep the parameter list as it was
written, so let add a b = ... is one node with two parameters. Folding those into nested arrow
types is inference's job, and turning them into nested one-parameter lambdas is the lowering's.
ELet and ETypeDecl both carry a body, and for a top-level binding that body is the whole
rest of the program: a box's bindings stack up by nesting inside each other. A let with nothing
after it gets EVar("Unit") for a body.
sealed interface Expr
A node of the surface tree, positioned by the SourceRange it was parsed from.
val range: SourceRange
data class EInt(val value: Long, override val range: SourceRange) : Expr
An integer literal.
data class EFloat(val value: Double, override val range: SourceRange) : Expr
A floating-point literal.
data class EBool(val value: Boolean, override val range: SourceRange) : Expr
true or false. The value comes from which keyword was read, never from the text.
data class EString(val value: String, override val range: SourceRange) : Expr
A string literal, with its quotes removed and its backslash escapes already decoded.
data class EVar(val name: String, override val range: SourceRange) : Expr
A name used as an expression.
It is also how two things that are not written as names arrive: () parses to EVar("Unit"),
and so does the body of a let with nothing after it.
data class ETuple(val elements: List<Expr>, override val range: SourceRange) : Expr
Two or more comma-separated expressions in parentheses.
A single parenthesised expression is grouping and leaves no node of its own, so this node always has at least two elements.
data class EList(val elements: List<Expr>, override val range: SourceRange) : Expr
A bracketed list of elements, which may be empty.
data class TypeDecl
A type declaration as written: the name being declared, the parameters it takes, and its
constructors.
This is the one surface node that survives both later stages unchanged. Inference and the lowering each keep it as it was parsed and record their own findings beside it, in TypedConstructor and CoreConstructor respectively.
data class TypeDecl(
val name: String,
val parameters: List<String>,
val constructors: List<ConstructorDecl>,
val range: SourceRange,
)
data class ConstructorDecl
One constructor of a TypeDecl, with its argument types as they were written.
data class ConstructorDecl(
val name: String,
val arguments: List<TypeRef>,
val range: SourceRange,
)
sealed interface TypeRef
A type as written inside a constructor's argument list.
val name: String
val range: SourceRange
data class TypeRefName
TypeRef's only case: a bare name.
Because there is no case for an application, a run of names reads as a run of separate arguments, and deciding what each name refers to is inference's job.
data class TypeRefName(
override val name: String,
override val range: SourceRange,
) : TypeRef
data class ETypeDecl
A type declaration together with the program that follows it, which is its body.
data class ETypeDecl(
val declaration: TypeDecl,
val body: Expr,
override val range: SourceRange,
) : Expr
data class ELet
A let binding together with the program that follows it.
parameters holds the function form's parameters as they were written, so let add a b = ... is
one node with two of them rather than a binding whose value is a lambda. For a top-level binding
the body is the whole rest of the program.
data class ELet(
val name: String,
val parameters: List<String>,
val value: Expr,
val body: Expr,
override val range: SourceRange,
) : Expr
data class ELambda
fun x y -> ..., keeping its parameter list as written.
Turning several parameters into nested one-parameter lambdas is the lowering's job, not the parser's.
data class ELambda(
val parameters: List<String>,
val body: Expr,
override val range: SourceRange,
) : Expr
data class ECall
One argument applied to one function.
Application is curried as it is parsed, so add 20 22 is ECall(ECall(add, 20), 22). The pipe
operator builds the same node with its left operand as the argument, which is why 10 |> add 5
and add 5 10 differ only in the ranges they record.
data class ECall(
val function: Expr,
val argument: Expr,
override val range: SourceRange,
) : Expr
data class EIf
if ... then ... else .... There is no one-armed form, so both branches are always present.
data class EIf(
val condition: Expr,
val thenBranch: Expr,
val elseBranch: Expr,
override val range: SourceRange,
) : Expr
sealed interface Pattern
The left-hand side of a match arm, positioned by the SourceRange it was parsed from.
val range: SourceRange
data class PWildcard(override val range: SourceRange) : Pattern
_: matches anything and binds nothing.
data class PVar(val name: String, override val range: SourceRange) : Pattern
A lower-case name: matches anything and binds it for the arm's body.
data class PInt(val value: Long, override val range: SourceRange) : Pattern
An integer literal, matching that value.
data class PBool(val value: Boolean, override val range: SourceRange) : Pattern
true or false, matching that value.
data class PString(val value: String, override val range: SourceRange) : Pattern
A string literal, escapes already decoded, matching that text.
data class PTuple(val elements: List<Pattern>, override val range: SourceRange) : Pattern
Two or more comma-separated patterns in parentheses. A single one is grouping.
data class PList(val elements: List<Pattern>, override val range: SourceRange) : Pattern
A bracketed pattern, matching a list of exactly that length. [] matches the empty list.
data class PConstructor
A constructor name and the patterns for its arguments.
An upper-case name on its own is this node with no arguments, which is how None is told apart
from a PVar that would match anything.
data class PConstructor(
val name: String,
val arguments: List<Pattern>,
override val range: SourceRange,
) : Pattern
data class PCons
head :: tail — matches any non-empty list, binding its first element and the rest.
data class PCons(
val head: Pattern,
val tail: Pattern,
override val range: SourceRange,
) : Pattern
data class MatchCase
One | pattern -> body arm of an EMatch.
data class MatchCase(
val pattern: Pattern,
val body: Expr,
val range: SourceRange,
)
data class EMatch
match scrutinee with followed by its arms.
The arms are kept in the order they were written, and nothing here checks that they cover the scrutinee's type or that a later one is reachable.
data class EMatch(
val scrutinee: Expr,
val cases: List<MatchCase>,
override val range: SourceRange,
) : Expr
data class RecordField
One name = value field of an ERecord.
data class RecordField(
val name: String,
val value: Expr,
val range: SourceRange,
)
data class ERecord
A braced record literal.
Fields keep their written order. A repeated field name is rejected while parsing, so the list a reader gets here has distinct names.
data class ERecord(
val fields: List<RecordField>,
override val range: SourceRange,
) : Expr
data class EField
target.field, reading one field out of a record.
data class EField(
val target: Expr,
val field: String,
override val range: SourceRange,
) : Expr
enum class BinaryOp(val text: String)
The infix operators, each carrying the text it is written as.
CONS is :: in expression position; the pattern that takes the same shape apart again is
PCons. Both are right associative, so a :: b :: rest builds and matches alike. CONCAT is
++, which joins two strings and only strings: a list is built with CONS.
enum class BinaryOp(val text: String) {
ADD("+"),
SUB("-"),
MUL("*"),
DIV("/"),
CONCAT("++"),
LT("<"),
LTE("<="),
GT(">"),
GTE(">="),
EQ("=="),
NEQ("!="),
AND("&&"),
OR("||"),
CONS("::"),
}
data class EBinary
An infix operator applied to two operands, with precedence already resolved into the shape.
data class EBinary(
val op: BinaryOp,
val left: Expr,
val right: Expr,
override val range: SourceRange,
) : Expr
data class ENegate
Prefix - over numbers and vectors.
data class ENegate(
val value: Expr,
override val range: SourceRange,
) : Expr