Code generation for MoonBit from annotated declarations.
| Layer | Package | What it does |
|---|---|---|
| Read | moonast/gen | Parses a file and gives every declaration as a plain view: Struct, Enum, Func. Types are kept as the source text that wrote them. |
| Write | moonast/quote | A template is real MoonBit with $holes. It is parsed, each hole is filled by replacing the node it stands for, and the official printer lays the result out. |
| Generate | moonast/gen, moonast/cli | render puts a header above the code; run is the whole main of a generator binary. |
// models.mbt
#demo.describe
pub struct Point {
x : Int
y : Int
}// src/main.mbt — give every annotated struct a `describe` method
struct Describe {}
impl @gen.Generator for Describe with fn name(_) {
"describe"
}
impl @gen.Generator for Describe with fn generate(_, decls) {
let out = []
for decl in decls {
guard decl is Struct(name~, fields~, attrs~, ..) &&
@attr.find(attrs, "demo.describe") is Some(_) else {
continue
}
let text = name + "(" + fields.map(f => f.name).join(", ") + ")"
let source =
#|fn $T::describe(self : $T) -> String {
#| ignore(self)
#| $text
#|}
out.push(
@quote.items(source)
.set("T", @quote.ident(name))
.set("text", @quote.string(text))
.render(),
)
}
@quote.join(out)
}
async fn main {
@cli.run(Describe::{}, @env.args()[1:])
}// moon.pkg of that package
import {
"moonbitstack/moonast/quote",
"moonbitstack/moonast/attr",
"moonbitstack/moonast/gen",
"moonbitstack/moonast/cli",
"moonbitlang/async",
"moonbitlang/core/env",
}// moon.mod of the project that uses the generator
rule(name: "describe", command: "moonx <module>@<version> -C \"$mod_dir\" \"$input\" -o \"$output\"")// moon.pkg of the package whose declarations it reads
dev_build(rule: "describe", input: "models.mbt", output: "describe.g.mbt")
formatter(ignore: [ "describe.g.mbt" ])// Generated by describe from models.mbt. Edit those files, not this one.
///|
fn Point::describe(self : Point) -> String {
ignore(self)
"Point(x, y)"
}impl @gen.Generator for Getters with fn generate(_, decls) {
let out = []
for decl in decls {
guard decl is Struct(name~, fields~, attrs~, ..) else { continue }
guard @attr.find(attrs, "demo.getters") is Some(annotation) else { continue }
let skip = @attr.Spec::new("demo.getters")
.many("skip", Ident)
.bind(annotation)
.strs("skip")
for f in fields {
guard !skip.contains(f.name) else { continue }
let source =
#|fn $T::$field(self : $T) -> $Ty {
#| self.$field
#|}
out.push(
@quote.items(source)
.set("T", @quote.ident(name))
.set("field", @quote.ident(f.name))
.set("Ty", @quote.ty(f.ty).render())
.render(),
)
}
}
@quote.join(out)
}#demo.getters(skip=token)
pub struct User {
id : Int
name : String
token : String
}// Generated by getters from models.mbt. Edit those files, not this one.
///|
fn User::id(self : User) -> Int {
self.id
}
///|
fn User::name(self : User) -> String {
self.name
}let source =
#|fn $T::$field(self : $T) -> $Ty {
#| self.$field
#|}
let code = @quote.items(source)
.set("T", @quote.ident("User"))
.set("field", @quote.ident("id"))
.set("Ty", @quote.ty("Int").render())
.render()
println(code)| Item | Takes | Gives |
|---|---|---|
| items(source) | MoonBit declarations with holes | a Template |
| expr(source) | one expression with holes | a Template |
| ty(source) | one type with holes | a Template |
| Template::set(hole, code) | a hole's name, a Code | the template, filled |
| Template::splice(hole, codes) | a list hole's name, codes | the template, spliced |
| Template::render() | — | a Code, or Refused |
| ident(name) | a name, or @pkg.name | a Code, or Refused |
| string(text) | any text | a Code |
| int(value), bool(value) | a literal | a Code |
| join(codes) | declaration codes | one Code, or Refused |
| Code::items() | — | the official syntax tree, when it is declarations |
| Code::to_string() | — | the source text |
| Item | Takes | Gives |
|---|---|---|
| name_of(attr) | an annotation | its full name, lib.name |
| find(attrs, name) | annotations, a name | the first one called that |
| all(attrs, name) | annotations, a name | every one called that |
| under(attrs, lib) | annotations, a prefix | every one under lib. |
| Spec::new(name) | an annotation's name | a spec with no parameters |
| Spec::positional(key, kind) | required, by position or name | the spec |
| Spec::required(key, kind) | required, by name | the spec |
| Spec::optional(key, kind) | may be left out | the spec |
| Spec::many(key, kind) | may be given again | the spec |
| Spec::bind(attr) | an annotation | its arguments, or Refused |
| Args::get, str, strs, bool, has | a parameter's key | what was given for it |
| Item | Takes | Gives |
|---|---|---|
| read(file, source) | a file name and its source | its declarations, or Refused |
| render(generator, inputs) | a generator and its inputs | the file to write |
| Generator::name() | — | the name in the header |
| Generator::generate(decls) | every declaration | the code to write |
| Decl | — | Struct, Enum, Func or Other |
| Decl::attrs(), Decl::raw() | — | the annotations on it, the syntax tree behind it |
| Field, Param, Pass | — | a struct field, a parameter, how it is passed |
| Item | Takes | Gives |
|---|---|---|
| run(generator, args) | a generator, [-C <dir>] <inputs...> -o <output> [--check] | writes the file, or refuses when it is stale |
| Hole | Takes | Written as | Gives |
|---|---|---|---|
| $T | a type or a constructor | fn f(x : $T) | fn f(x : Int) |
| $name | a value, an expression, or a name | $value + 1 | 41 + 1 |
| @pkg.$name | a name inside a package | app : @web.$T | app : @web.App |
| $name* | a list of them | [$items*] | [1, 2, 3] |
| $name in a string | a name or a string, as text | "field $key" | "field first name" |
| $$ | a literal dollar sign | "$$5" | "$5" |
| From | Refused when |
|---|---|
| quote | the template is not MoonBit, a hole is left empty, a fill names no hole, or a fill does not fit where it stands |
| attr | the payload cannot be read, a required parameter is missing, one is unknown, mistyped, or given twice |
| gen | an input file is not MoonBit |
| cli | the command line is wrong, or --check found the output out of date |
| Directory | What it is |
|---|---|
| src/examples/tour | The whole surface in one run: read, bind, generate. |
| src/examples/describe | A minimal derive generator: #demo.describe gives a struct describe. |
| src/examples/getters | A generator whose annotation takes arguments: #demo.getters(skip=token). |
| src/examples/play | The same three packages compiled to JavaScript, which is what the site's playground runs. |
Install
Download zipCode generation for MoonBit from annotated declarations.