// The wap abstract syntax tree.
//
// Two properties are load-bearing and should survive any edit here.
//
// FIRST, everything is public and constructible. A project that generates wap
// -- a DSL compiler, a schema-to-wasm tool -- builds these values directly and
// never produces source text. That is why every type is `pub(all)` and why the
// constructors in `build.mbt` default their spans: string codegen is not a
// supported way to use this compiler, it is the thing this AST exists to avoid.
//
// SECOND, a wap program is made of expressions, not statements. Blocks, loops
// and conditionals are expressions whose value is the value of the last
// expression in them, exactly as in Wax, so the lowering is a translation and
// not a restructuring.
///|
/// A source span, as byte offsets into the file the node came from.
///
/// This is `marianoguerra/error-report`'s span, so a wap diagnostic and a
/// diagnostic from any other library that uses error-report render through the
/// same code. `nowhere` is the span a generated node carries; the lowering
/// gives those fresh synthetic locations so that two generated bindings never
/// collide (see `lower`, and the `AmbiguousBinding` note in AGENTS.md).
pub using @er {type Span}
///|
/// The span of a node that was never written down.
pub let nowhere : Span = { start: -1, len: 0, }
///|
/// True when a span points at real source text.
pub fn has_source(s : Span) -> Bool {
s.start >= 0
}
// ---------------------------------------------------------------- types
///|
/// A wap type.
///
/// The signed/unsigned distinction lives here rather than on the operators,
/// which is the whole reason wap can spell integer comparison `<` where was has
/// to spell it `<` or `<$`. `U32` and `I32` are both wasm i32; they differ only
/// in which instruction an operator on them selects.
pub(all) enum Type {
I8
I16
I32
I64
U8
U16
U32
U64
F32
F64
Bool
Char
/// A reference to a declared type: a record, an array alias, an enum, a set.
Named(String)
/// `t?` -- a nullable reference.
Nullable(Type)
/// `[t]` written inline rather than through an alias.
ArrayOf(Type)
/// `(a, b)` -- flattened at every use, never boxed.
Tuple(Array[Type])
/// `fn(a, b) -> c`
Func(params~ : Array[Type], results~ : Array[Type])
/// `any` -- the top of the reference hierarchy.
Any
/// `eq` -- the part of it that supports identity comparison.
Eq
/// `i31` -- a boxed 31-bit integer.
I31
} derive(Eq, @debug.Debug)
///|
/// What a `type` declaration introduces.
pub(all) enum TypeDef {
/// `{ x :: i32, y :: mut f64 }`, optionally extending another record.
Record(parent~ : String?, fields~ : Array[Field])
/// `enum { red, green = 4, blue }`
Enum(Array[(String, Int?)])
/// `set[color]` -- a bitmask over an enumeration.
SetOf(String)
/// `1 .. 31` -- an integer with known bounds.
Subrange(low~ : Int, high~ : Int)
/// `[u8]`, `fn(i32) -> i32`, or any other type expression.
Alias(Type)
} derive(Eq, @debug.Debug)
///|
/// One field of a record.
pub(all) struct Field {
name : String
mut_ : Bool
typ : Type
span : Span
} derive(Eq, @debug.Debug)
// ---------------------------------------------------------------- operators
///|
/// A binary operator, before signedness is resolved.
///
/// `Div`, `Rem`, `Shr` and the four comparisons become signed or unsigned wasm
/// instructions according to the type of their operands; the parser never has
/// to decide, and there is no wap spelling that lets it.
pub(all) enum BinOp {
Add
Sub
Mul
Div
Rem
Shl
Shr
BitAnd
BitOr
BitXor
Eq
Ne
Lt
Gt
Le
Ge
} derive(Eq, @debug.Debug)
///|
/// A prefix operator.
pub(all) enum UnOp {
Neg
Not
} derive(Eq, @debug.Debug)
///|
/// How an assignment combines with the old value.
pub(all) enum AssignOp {
Set
OpSet(BinOp)
} derive(Eq, @debug.Debug)
// ---------------------------------------------------------------- patterns
///|
/// A `match` arm's pattern.
pub(all) enum Pattern {
/// `c :: circle` -- a type test that binds.
PType(name~ : String?, typ~ : Type)
/// `null`
PNull
/// `7`
PInt(String)
/// `2 .. 6`
PRange(low~ : String, high~ : String)
/// `{1, 7}` -- a set of labels, which is what a case label list has always been.
PSet(Array[Pattern])
/// `_`
PWild
} derive(Eq, @debug.Debug)
// ---------------------------------------------------------------- expressions
///|
/// An expression with the span it was written at.
pub(all) struct Node {
it : Expr
span : Span
} derive(Eq, @debug.Debug)
///|
/// One arm of a `match`.
pub(all) struct Arm {
pat : Pattern
body : Array[Node]
span : Span
} derive(Eq, @debug.Debug)
///|
/// A `let`/`var` binding target.
pub(all) struct Binder {
name : String
typ : Type?
span : Span
} derive(Eq, @debug.Debug)
///|
/// Everything a wap body can contain.
pub(all) enum Expr {
// -- literals -----------------------------------------------------------
/// An integer literal, kept as written. Parsing it here would lose the
/// flexible-literal typing the Wax checker does later.
Int(String)
Float(String)
BoolLit(Bool)
CharLit(Char)
/// A string literal, lowered to an `[u8]` array.
StrLit(String)
Null
/// `(a, b)` -- flattened at the point of use.
TupleLit(Array[Node])
/// `{red, blue}` in a set-typed position.
SetLit(Array[Node])
// -- names --------------------------------------------------------------
Var(String)
Field(Node, String)
Index(Node, Node)
// -- construction -------------------------------------------------------
/// `point{x: 1, y}` -- a field with no value is punned.
RecordLit(typ~ : String?, fields~ : Array[(String, Node?)])
/// `point{..}`
RecordDefault(typ~ : String?)
/// `ints[1, 2, 3]`
ArrayLit(typ~ : String?, items~ : Array[Node])
/// `bytes[0 ** n]`
ArrayRepeat(typ~ : String?, value~ : Node, count~ : Node)
// -- application --------------------------------------------------------
Call(Node, Array[Node])
/// `xs.len()`, `p.area()` -- resolved to a field access plus call, an
/// intrinsic, or a method, by the lowering.
MethodCall(Node, String, Array[Node])
// -- operators ----------------------------------------------------------
Bin(BinOp, Node, Node)
Un(UnOp, Node)
/// `&&` and `||`, which short-circuit and therefore become `if`.
AndAlso(Node, Node)
OrElse(Node, Node)
/// `x in s` -- set membership.
InSet(Node, Node)
/// `e as t`
Cast(Node, Type)
/// `e is t`
Test(Node, Type)
/// `e!`
NonNull(Node)
// -- binding and assignment ---------------------------------------------
/// `let (q, r) = e` / `var x :: i32 = e`
Bind(binders~ : Array[Binder], value~ : Node?, mutable~ : Bool)
/// `x = e`, `x += e`, `(x, y) = (y, x)`
Assign(targets~ : Array[Node], op~ : AssignOp, value~ : Node)
// -- control ------------------------------------------------------------
/// A sequence whose value is its last element.
Block(Array[Node])
/// `if c | a | b` and the multi-arm `if | c: a | d: b | _: c`. The final
/// arm may have no condition, and is then the else.
If(arms~ : Array[(Node?, Array[Node])], typ~ : Type?)
While(label~ : String?, cond~ : Node, step~ : Node?, body~ : Array[Node])
/// `for i in lo .. hi by n:`
ForRange(
label~ : String?,
var_~ : String,
from~ : Node,
to~ : Node,
inclusive~ : Bool,
by~ : Node?,
body~ : Array[Node]
)
/// `for x in xs:`
ForIn(label~ : String?, var_~ : String, seq~ : Node, body~ : Array[Node])
Loop(label~ : String?, body~ : Array[Node])
Break(String?)
Continue(String?)
Return(Node?)
Match(scrutinee~ : Node, arms~ : Array[Arm], typ~ : Type?)
Unreachable
Nop
/// `_ = e` -- evaluate and drop.
Drop(Node)
} derive(Eq, @debug.Debug)
// ---------------------------------------------------------------- declarations
///|
/// A function or method, defined or merely declared.
pub(all) struct FnDecl {
name : String
/// Set when the function came from an `impl` block: the receiver's type.
receiver : String?
params : Array[Param]
results : Array[Type]
/// `None` for a declaration with no body, which is how an import is written.
body : Array[Node]?
export_name : String?
import_name : String?
is_start : Bool
/// Whether another module may name this one. Wasm exports are a different
/// thing entirely: `export "n"` is an ABI, `pub` is visibility.
is_pub : Bool
span : Span
} derive(Eq, @debug.Debug)
///|
/// One parameter.
pub(all) struct Param {
name : String
typ : Type
span : Span
} derive(Eq, @debug.Debug)
///|
/// A global constant.
pub(all) struct ConstDecl {
name : String
typ : Type?
value : Node
export_name : String?
import_name : String?
is_pub : Bool
span : Span
} derive(Eq, @debug.Debug)
///|
/// A top-level declaration.
pub(all) enum Decl {
/// `module vector`
ModuleName(String)
/// `import collections.hashing`
Import(Array[String])
/// `import was "kernels.was": ...` -- signatures for functions defined in
/// was and compiled alongside. Nothing is generated for these; they are a
/// type ascription for a name the linker will already have.
ImportWas(file~ : String, funcs~ : Array[FnDecl], consts~ : Array[ConstDecl])
/// `import "env": ...` -- a wasm import.
ImportHost(
module_~ : String,
funcs~ : Array[FnDecl],
consts~ : Array[ConstDecl]
)
Const(ConstDecl)
TypeD(name~ : String, def~ : TypeDef, is_pub~ : Bool, span~ : Span)
Fn(FnDecl)
/// `impl circle: ...`
Impl(typ~ : String, methods~ : Array[FnDecl], span~ : Span)
} derive(Eq, @debug.Debug)
///|
/// A whole wap compilation unit.
pub(all) struct Module {
name : String
decls : Array[Decl]
} derive(Eq, @debug.Debug)