API reference
Every public type, function, method, trait and error, one plate per package and
alphabetical within each group. Import one package at a time:
moonbitstack/moonast/quote, /attr, /gen,
/cli.
quotemoonbitstack/moonast/quote
Templates: real MoonBit with $holes. A template is parsed with its holes standing as placeholders, each fill is put where its placeholder stands, and the result is printed by the official formatter.
Values
A boolean literal.
inspect(@quote.bool(true).to_string(), content="true")A byte string literal, b"...", holding text in UTF-8. Every byte outside printable ASCII is written \xNN, so the literal says the same thing in a source file of any encoding; string is the text form of the same value.
inspect(@quote.bytes("max-retries").to_string(), content="b\"max-retries\"")
inspect(
@quote.bytes("坐标").to_string(),
content="b\"\\xe5\\x9d\\x90\\xe6\\xa0\\x87\"",
)A template of match cases, for the hole a match has where its case list goes. Each case is pattern => body, on its own line, exactly as it is written between the braces of a match.
let cases = @quote.cases("$pattern => $value")
.set("pattern", @quote.ident("Empty"))
.set("value", @quote.int(0))
.render()
inspect(cases.to_string(), content="Empty => 0")A template of one expression.
let code = @quote.expr("$a + $b")
.set("a", @quote.int(1))
.set("b", @quote.int(2))
.render()
inspect(code.to_string(), content="1 + 2")A template of record fields, for the hole a record has where its field list goes. Each field is label: value; the same code fills the arguments of a constructor, where a labelled argument is written the same way.
let fields = @quote.fields("$label: $value")
.set("label", @quote.ident("x"))
.set("value", @quote.int(1))
.render()
inspect(fields.to_string(), content="x: 1")A name: a value, a function, a field, a type or a constructor, optionally qualified as @pkg.name. Refused unless it is a well-formed identifier.
inspect(@quote.ident("answer").to_string(), content="answer")
inspect(@quote.ident("@web.App").to_string(), content="@web.App")An integer literal.
inspect(@quote.int(42).to_string(), content="42")A template of top-level declarations.
let code = @quote.items("fn $name() -> $T {\n $value\n}")
.set("name", @quote.ident("answer"))
.set("T", @quote.ident("Int"))
.set("value", @quote.int(42))
.render()
inspect(code.to_string(), content="///|\nfn answer() -> Int {\n 42\n}\n")Several codes of top-level declarations as one, in order.
let a = @quote.items("fn a() -> Unit { () }").render()
let b = @quote.items("fn b() -> Unit { () }").render()
inspect(
@quote.join([a, b]).to_string(),
content="///|\nfn a() -> Unit {\n ()\n}\n\n///|\nfn b() -> Unit {\n ()\n}\n",
)A string literal holding text; quoting and escaping happen when it is printed.
inspect(@quote.string("坐标").to_string(), content="\"坐标\"")A template of one type.
let code = @quote.ty("Array[$T]").set("T", @quote.ident("User")).render()
inspect(code.to_string(), content="Array[User]")Types
A piece of MoonBit: top-level declarations, an expression, a type, a name, or the text of a string literal. Codes come from rendering a template or from the literal constructors here, and fill the holes of other templates.
The official syntax tree behind a code of declarations, for work this package does not cover.
let code = @quote.items("fn a() -> Unit { () }").render()
inspect(code.items() is Some(_), content="true")Writes the value to a logger, as <code>Show</code> defines it.
The value as a string, as <code>Show</code> defines it.
A piece of MoonBit with holes, waiting to be filled and rendered.
A hole is $name; write it in the case the position wants, $T where a type or constructor goes and $name where a value or a name goes. $name* takes a list and expands among statements, array elements, positional arguments, a match's cases and a record's fields. $$ is a literal dollar sign.
Parse the template with its holes standing as placeholders, put each fill where its placeholder stands, and give back the result. Refused when a hole is left empty, a fill names no hole, a fill does not fit where it stands, or the template is not MoonBit.
let text = @quote.items("fn $name() -> Unit { () }").render().to_string() catch {
e => e.to_string()
}
inspect(text, content="hole $name was given nothing")Fill $hole with code.
let t = @quote.items("fn $name() -> Unit { () }").set(
"name",
@quote.ident("answer"),
)
inspect(
t.render().to_string(),
content="///|\nfn answer() -> Unit {\n ()\n}\n",
)Fill $hole* with codes, in order.
let code = @quote.expr("[$items*]")
.splice("items", [@quote.int(1), @quote.int(2), @quote.int(3)])
.render()
inspect(code.to_string(), content="[1, 2, 3]")Errors
Why a template or a code was not accepted. Each variant is named after the thing that was wrong.
- Template(msg~ : String, line~ : Int32, column~ : Int32) — The template is not MoonBit once its holes are in place.
- Unfilled(hole~ : String) — A hole in the template was given nothing.
- Unused(hole~ : String) — Something was given for a hole the template does not have.
- Misfit(hole~ : String, expected~ : String) — A hole was given a code of the wrong kind for where it stands.
- Name(name~ : String) —
identwas given something that is not an identifier. - Kind(expected~ : String) —
joinwas given a code that is not declarations.
Writes the value to a logger, as <code>Show</code> defines it.
The value as a <code>Repr</code>, as <code>Debug</code> defines it.
The value as a string, as <code>Show</code> defines it.
attrmoonbitstack/moonast/attr
Annotations as typed arguments. Find #lib.name(...) on a declaration and bind its payload against a spec; every refusal carries the position of the annotation.
Values
Every annotation called name, in source order.
let source = "#app.get(\"/users\", tag=\"users\")\nfn list_users() -> Unit {}"
let impls = @parser.parse_string(source).0
guard impls is @list.More(impl_, tail=@list.Empty) else {
abort("one declaration")
}
guard impl_ is @syntax.TopFuncDef(fun_decl={ attrs, .. }, ..) else {
abort("a function")
}
inspect(@attr.all(attrs, "app.get").length(), content="1")The first annotation called name.
let source = "#app.get(\"/users\", tag=\"users\")\nfn list_users() -> Unit {}"
let impls = @parser.parse_string(source).0
guard impls is @list.More(impl_, tail=@list.Empty) else {
abort("one declaration")
}
guard impl_ is @syntax.TopFuncDef(fun_decl={ attrs, .. }, ..) else {
abort("a function")
}
inspect(@attr.find(attrs, "app.get") is Some(_), content="true")The annotations starting #lib. whose payload the parser could not read, in source order. A key that is a keyword does this: #app.get(type="x"). under, find and all go by the parsed name and pass over them, so a generator asks here first and reports them, rather than ignoring what was written.
let source = "#app.get(type=\"x\")\nfn list_users() -> Unit {}"
let impls = @parser.parse_string(source).0
guard impls is @list.More(impl_, tail=@list.Empty) else {
abort("one declaration")
}
guard impl_ is @syntax.TopFuncDef(fun_decl={ attrs, .. }, ..) else {
abort("a function")
}
inspect(@attr.under(attrs, "app").length(), content="0")
inspect(@attr.malformed(attrs, "app").length(), content="1")The full name of an annotation, lib.name for #lib.name(...); None when the parser could not read its payload.
let source = "#app.get(\"/users\", tag=\"users\")\nfn list_users() -> Unit {}"
let impls = @parser.parse_string(source).0
guard impls is @list.More(impl_, tail=@list.Empty) else {
abort("one declaration")
}
guard impl_ is @syntax.TopFuncDef(fun_decl={ attrs, .. }, ..) else {
abort("a function")
}
guard @attr.find(attrs, "app.get") is Some(a) else { abort("annotated") }
inspect(@attr.name_of(a).unwrap_or(""), content="app.get")Every annotation whose name starts with lib., in source order: a generator for #app.get, #app.post and the rest asks once for app.
let source = "#app.get(\"/users\", tag=\"users\")\nfn list_users() -> Unit {}"
let impls = @parser.parse_string(source).0
guard impls is @list.More(impl_, tail=@list.Empty) else {
abort("one declaration")
}
guard impl_ is @syntax.TopFuncDef(fun_decl={ attrs, .. }, ..) else {
abort("a function")
}
inspect(@attr.under(attrs, "app").length(), content="1")Types
An annotation's arguments after Spec::bind: every required one is here and every one has the kind its spec gives.
- name : String
- line : Int32
- column : Int32
- file : String
The boolean given for key.
The values given for key, in order; empty when it was not given.
Whether key was given.
let source = "#app.get(\"/users\", tag=\"users\")\nfn list_users() -> Unit {}"
let impls = @parser.parse_string(source).0
guard impls is @list.More(impl_, tail=@list.Empty) else {
abort("one declaration")
}
guard impl_ is @syntax.TopFuncDef(fun_decl={ attrs, .. }, ..) else {
abort("a function")
}
guard @attr.find(attrs, "app.get") is Some(a) else { abort("annotated") }
let args = @attr.Spec::new("app.get")
.positional("path", Str)
.many("tag", Str)
.bind(a)
inspect(args.has("tag"), content="true")
inspect(args.has("page"), content="false")The string given for key.
let source = "#app.get(\"/users\", tag=\"users\")\nfn list_users() -> Unit {}"
let impls = @parser.parse_string(source).0
guard impls is @list.More(impl_, tail=@list.Empty) else {
abort("one declaration")
}
guard impl_ is @syntax.TopFuncDef(fun_decl={ attrs, .. }, ..) else {
abort("a function")
}
guard @attr.find(attrs, "app.get") is Some(a) else { abort("annotated") }
let args = @attr.Spec::new("app.get")
.positional("path", Str)
.many("tag", Str)
.bind(a)
inspect(args.str("path").unwrap_or(""), content="/users")Every string given for a repeated key.
let source = "#app.get(\"/users\", tag=\"users\")\nfn list_users() -> Unit {}"
let impls = @parser.parse_string(source).0
guard impls is @list.More(impl_, tail=@list.Empty) else {
abort("one declaration")
}
guard impl_ is @syntax.TopFuncDef(fun_decl={ attrs, .. }, ..) else {
abort("a function")
}
guard @attr.find(attrs, "app.get") is Some(a) else { abort("annotated") }
let args = @attr.Spec::new("app.get")
.positional("path", Str)
.many("tag", Str)
.bind(a)
inspect(args.strs("tag").join(", "), content="users")What a parameter accepts.
- Str
- Bool
- Ident
- Path
- Type
Whether two values are equal, as <code>Eq</code> defines it.
Whether two values differ, as <code>Eq</code> defines it.
The value as a <code>Repr</code>, as <code>Debug</code> defines it.
The parameters one annotation takes. Built by chaining:
Spec::new("app.get").positional("path", Str).optional("summary", Str).many("tags", Str)The annotation's arguments, checked against the spec.
let source = "#app.get(\"/users\", tag=\"users\")\nfn list_users() -> Unit {}"
let impls = @parser.parse_string(source).0
guard impls is @list.More(impl_, tail=@list.Empty) else {
abort("one declaration")
}
guard impl_ is @syntax.TopFuncDef(fun_decl={ attrs, .. }, ..) else {
abort("a function")
}
guard @attr.find(attrs, "app.get") is Some(a) else { abort("annotated") }
let spec = @attr.Spec::new("app.get").positional("path", Str).many("tag", Str)
let args = spec.bind(a)
inspect(args.str("path").unwrap_or(""), content="/users")
inspect(args.strs("tag").join(", "), content="users")
Pass the file the annotation was read from and every refusal names it, so the line and column in the message can be opened:
let source = "#app.get\nfn list_users() -> Unit {}"
let impls = @parser.parse_string(source).0
guard impls is @list.More(impl_, tail=@list.Empty) else {
abort("one declaration")
}
guard impl_ is @syntax.TopFuncDef(fun_decl={ attrs, .. }, ..) else {
abort("a function")
}
guard @attr.find(attrs, "app.get") is Some(a) else { abort("annotated") }
let refused = try {
let _ = @attr.Spec::new("app.get")
.positional("path", Str)
.bind(a, file="models.mbt")
"bound"
} catch {
e => e.to_string()
}
inspect(refused, content="models.mbt:1:1: path is required")A parameter that may be given any number of times, tags="a", tags="b", which is how a list is written where the grammar has no arrays. With positional, the list is written as bare arguments, #serde.derive(Serialize, Deserialize), and it takes every one of them from where it starts.
A parameter that may be left out.
A parameter that may be given by position, in the order these are declared, or by name. It is required unless optional says otherwise, which is what lets #orm.table("heroes") and a bare #orm.table both stand.
A required parameter given by name.
One argument, as its kind reads it.
- Str(String)
- Bool(Bool)
- Ident(String)
- Type(String)
Whether two values are equal, as <code>Eq</code> defines it.
Whether two values differ, as <code>Eq</code> defines it.
The value as a <code>Repr</code>, as <code>Debug</code> defines it.
Errors
Why an annotation's arguments were not accepted, with where the annotation stands in its file. file is empty when the caller did not say which file it read the annotation from, and then the message names the position alone.
- Malformed(raw~ : String, line~ : Int32, column~ : Int32, file~ : String) — The payload is outside the annotation grammar: arrays, numbers, a qualified or generic type cannot be written, and a keyword cannot be a label.
- Missing(key~ : String, line~ : Int32, column~ : Int32, file~ : String) — A required parameter was not given.
- Unknown(key~ : String, line~ : Int32, column~ : Int32, file~ : String) — A parameter the spec does not have.
- Mistyped(key~ : String, expected~ : String, line~ : Int32, column~ : Int32, file~ : String) — A parameter of the wrong kind.
- Repeated(key~ : String, line~ : Int32, column~ : Int32, file~ : String) — A parameter given twice that may only be given once.
Writes the value to a logger, as <code>Show</code> defines it.
The value as a <code>Repr</code>, as <code>Debug</code> defines it.
The value as a string, as <code>Show</code> defines it.
genmoonbitstack/moonast/gen
Declarations read from source into plain views, the Generator trait, and render, which gives the file a generator writes. No file access here.
Values
The declarations of one file, in source order. Refused when the file is not MoonBit, with the first problem's position.
let source = "pub struct Point {\n x : Int\n y : Int\n}"
guard @gen.read("models.mbt", source) is [Struct(name~, fields~, ..)] else {
abort("a struct")
}
inspect(name, content="Point")
inspect(fields.map(f => f.name).join(", "), content="x, y")The file a generator writes for inputs: a one-line header naming the generator and the inputs, then the code. The same inputs give the same bytes, so a check can compare them.
Types
One variant of an enum, and the annotations written on it. Its payload is Empty, Tuple or Record; an enum inside an enum's variant would be a different type altogether, so Cases never appears here.
- name : String
- payload : Shape
- attrs : @list.List[@attribute.Attribute]
The value as a <code>Repr</code>, as <code>Debug</code> defines it.
A top-level declaration as a generator usually needs it. Types are kept as their source text, which is what a generator splices into new code; raw is the official syntax tree for anything the view leaves out.
- Struct(name~ : String, fields~ : Array[Field], attrs~ : @list.List[@attribute.Attribute], public~ : Bool, file~ : String, raw~ : @syntax.Impl)
- Enum(name~ : String, constructors~ : Array[String], cases~ : Array[Case], attrs~ : @list.List[@attribute.Attribute], public~ : Bool, file~ : String, raw~ : @syntax.Impl)
- Func(owner~ : String?, name~ : String, params~ : Array[Param], returns~ : String?, is_async~ : Bool, attrs~ : @list.List[@attribute.Attribute], public~ : Bool, file~ : String, raw~ : @syntax.Impl)
- Other(file~ : String, attrs~ : @list.List[@attribute.Attribute], raw~ : @syntax.Impl) — Anything else, with the annotations on it: a tuple struct is here, since
Structcovers the record case only, and so is animplor atrait.
The annotations on a declaration, whatever the view makes of its body.
let source = "#demo.describe\npub struct Point {\n x : Int\n}"
guard @gen.read("models.mbt", source) is [decl] else {
abort("one declaration")
}
inspect(@attr.find(decl.attrs(), "demo.describe") is Some(_), content="true")The file the declaration was read from, as read was told it. A refusal a generator writes names this file, so a reader can open the line it points at.
let source = "#demo.describe\npub struct Point {\n x : Int\n}"
guard @gen.read("src/models.mbt", source) is [decl] else {
abort("one declaration")
}
inspect(decl.file(), content="src/models.mbt")Whether the declaration is pub, for every kind the view has a variant for and for the tuple structs and traits that land in Other.
The name a declaration introduces: a type's, a function's or a trait's. Empty for one that introduces none, such as an impl. A tuple struct and a trait have no variant of their own, so this is how a generator learns their names without reading raw itself.
let source = "pub struct Rgb(Int, Int, Int)\n\npub fn f() -> Unit { () }"
let names = @gen.read("m.mbt", source).map(decl => decl.name())
inspect(names.join(", "), content="Rgb, f")The generic parameters of a type declaration, as written: ["A", "B"] for struct Pair[A, B]. Empty for a function, whose own parameters live on Func.
guard @gen.read("m.mbt", "pub struct Pair[A, B] {\n first : A\n}") is [decl] else {
abort("a struct")
}
inspect(decl.params().join(", "), content="A, B")What a function says after its return type, as source: empty, raise, raise E or raise? E, so a wrapper can say the same. A type with arguments cannot be an error type, so the name is all there is to print. Empty for anything that is not a function.
guard @gen.read("m.mbt", "fn f() -> Unit raise @a.Bad {}") is [decl] else {
abort("a function")
}
inspect(decl.raises(), content="raise @a.Bad")The official syntax tree behind the view.
The body of a type declaration, tuple structs included: Struct covers the record case, and this answers the other two. None for a function or a declaration that is not a type at all.
let source = "pub struct Rgb(Int, Int, Int)"
guard @gen.read("m.mbt", source) is [decl] else { abort("a declaration") }
inspect(
match decl.shape() {
Some(Tuple(arity~)) => "tuple of \{arity}"
_ => "not a tuple struct"
},
content="tuple of 3",
)A field of a struct, with its type as written in the source.
- name : String
- ty : String
- mutable : Bool
- optional : Bool
- attrs : @list.List[@attribute.Attribute]
The value as a <code>Repr</code>, as <code>Debug</code> defines it.
A function parameter: _ has no name, and an unannotated one no type.
- name : String?
- ty : String?
- pass : Pass
A parameter as a wrapper's signature declares it: a : Int, a~ : Int, a? : Int. A parameter with a default comes out optional, since the default itself stays with the function being wrapped. None when it has no name or no type, so there is nothing to pass along.
guard @gen.read(
"m.mbt", "fn f(a : Int, b~ : String, c~ : Bool = true) -> Unit {}",
)
is [Func(params~, ..)] else {
abort("a function")
}
inspect(
params.map(p => p.declared().unwrap_or("")).join(", "),
content="a : Int, b~ : String, c? : Bool",
)
inspect(
params.map(p => p.passed().unwrap_or("")).join(", "),
content="a, b~, c?",
)A parameter as a wrapper passes it on: a, a~, a?. None when it has no name.
How a function parameter is passed.
- Positional
- Labelled
- Optional
- Defaulted
Whether two values are equal, as <code>Eq</code> defines it.
Whether two values differ, as <code>Eq</code> defines it.
The value as a <code>Repr</code>, as <code>Debug</code> defines it.
The body of a type declaration: what a struct is made of, or the cases an enum has. One type for both, because a variant carries the same three shapes a struct can have — nothing, a run of positional types, or named fields.
let source =
#|pub enum Shape {
#| Empty
#| Circle(Double)
#| Rect(Double, Double)
#| Tagged(name~ : String, size~ : Int)
#|}
guard @gen.read("m.mbt", source) is [Enum(cases~, ..)] else {
abort("an enum")
}
let shapes = cases.map(c => {
match c.payload {
Empty => "empty"
Tuple(arity~) => "tuple of \{arity}"
Record(fields~) => "record of \{fields.length()}"
_ => "cases"
}
})
inspect(
shapes.join(", "),
content="empty, tuple of 1, tuple of 2, record of 2",
)- Empty
- Tuple(arity~ : Int32) —
struct Rgb(Int, Int, Int), orCircle(Double): a newtype when the count is one. A generator that writesself.0andself.1needs the count and nothing else; one that needs the types reads them offDecl::raw. - Record(fields~ : Array[Field]) —
struct Point { x : Int }, orTagged(name~ : String): the fields, with their types as written in the source. - Cases(cases~ : Array[Case]) —
enum Shape { ... }: its variants, in source order.
The value as a <code>Repr</code>, as <code>Debug</code> defines it.
Traits
A generator: given every declaration of its input files, the code to write. One call sees all of them, so a derive can answer per declaration and a registrar can gather every annotated function into one table.
Every declaration of the inputs, in one call: the code to write for all of them.
Short and stable: it goes in the header of every file it writes.
Errors
Why a generator's inputs were not read.
- Syntax(file~ : String, line~ : Int32, column~ : Int32, msg~ : String) — An input file is not MoonBit.
Writes the value to a logger, as <code>Show</code> defines it.
The value as a <code>Repr</code>, as <code>Debug</code> defines it.
The value as a string, as <code>Show</code> defines it.
climoonbitstack/moonast/cli
The command line of a generator binary: it reads the inputs, has moonast/gen render them, and writes the output or reports that it is stale.
Values
Run generator the way a dev_build rule calls it: read every input, render them, and write the output; with --check, only compare and refuse when the output is stale. args are the arguments after the program name; -C <dir> makes relative paths relative to dir.
Errors
Why a generator run stopped.
- Usage(msg~ : String) — The command line is not
[-C <dir>] <inputs...> -o <output> [--check]. - Stale(output~ : String) —
--checkfound the output out of date with its inputs. - Generator(msg~ : String) — The generator refused what it read, in its own words.
Writes the value to a logger, as <code>Show</code> defines it.
The value as a <code>Repr</code>, as <code>Debug</code> defines it.
The value as a string, as <code>Show</code> defines it.