///|
/// `#rpc.message`, `#rpc.enum` and `#rpc.service` on types, `#rpc.field`/
/// `#rpc.skip` on fields, `#rpc.value` on enum constructors, `#rpc.method` on a
/// service's methods, turned into each message's `Message`, each enum's `Enum`,
/// each service's `register` and client, and the file's `descriptor()`.
priv struct Rpc {}
///|
impl @gen.Generator for Rpc with fn name(_) {
"moonrpc"
}
///|
let message_spec : @attr.Spec = @attr.Spec::new("rpc.message").optional(
"name",
Str,
)
///|
let enum_spec : @attr.Spec = @attr.Spec::new("rpc.enum").optional("name", Str)
///|
let value_spec : @attr.Spec = @attr.Spec::new("rpc.value")
.optional("number", Str)
.optional("name", Str)
///|
let field_spec : @attr.Spec = @attr.Spec::new("rpc.field")
.optional("number", Str)
.optional("name", Str)
///|
let skip_spec : @attr.Spec = @attr.Spec::new("rpc.skip")
///|
let service_spec : @attr.Spec = @attr.Spec::new("rpc.service").optional(
"name",
Str,
)
///|
let method_spec : @attr.Spec = @attr.Spec::new("rpc.method").optional(
"name",
Str,
)
///|
fn at(attr : @attribute.Attribute, file : String) -> String {
"\{file}:\{attr.loc.start.lnum}:\{attr.loc.start.column()}"
}
///|
/// The `#rpc.*` attributes among `attrs`, refusing one that does not parse.
fn rpc_attrs(
attrs : @list.List[@attribute.Attribute],
file : String,
) -> Array[@attribute.Attribute] raise {
for attr in attrs {
if @attr.name_of(attr) is None && attr.raw.has_prefix("#rpc.") {
fail("\{at(attr, file)}: `\{attr.raw}` does not parse as an annotation")
}
}
@attr.under(attrs, "rpc")
}
///|
/// The one attribute among `found`, which must be one of `names`.
fn single(
found : Array[@attribute.Attribute],
names : Array[String],
what : String,
file : String,
) -> @attribute.Attribute? raise {
for attr in found {
guard @attr.name_of(attr) is Some(n) && names.contains(n) else {
let n = @attr.name_of(attr).unwrap_or("?")
fail("\{at(attr, file)}: `#\{n}` does not go on \{what}")
}
}
match found {
[] => None
[one] => Some(one)
[_, second, ..] => fail("\{at(second, file)}: one `#rpc.*` per declaration")
}
}
///|
/// A service: its full proto name, whether its struct is public, and what the
/// generator writes for it.
priv struct Service {
name : String
public : Bool
register : Array[String]
methods : Array[String]
calls : Array[String]
}
///|
/// What a message's descriptor is made of. It is assembled once every message
/// is known, so a nested one can go inside its parent.
priv struct Described {
fields : Array[String]
entries : Array[String]
oneofs : Array[String]
}
///|
/// The generated file, built up declaration by declaration. `messages` and
/// `enums` map MoonBit names to proto names; `described` and `enum_descs` are
/// keyed by proto name, in the order of `message_order` and `enum_order`.
priv struct Out {
messages : Map[String, String]
enums : Map[String, String]
services : Map[String, Service]
order : Array[String]
mut pkg : String?
impls : Array[String]
described : Map[String, Described]
message_order : Array[String]
enum_descs : Map[String, String]
enum_order : Array[String]
}
///|
/// A message's or enum's fully qualified name, as descriptors write it.
fn full_name(out : Out, proto : String) -> String {
match out.pkg {
Some(p) if p != "" => "." + p + "." + proto
_ => "." + proto
}
}
///|
/// The fully qualified name of the message or enum a base refers to; empty for
/// a scalar.
fn composite_name(out : Out, base : Base) -> String {
match base {
Message(m) => full_name(out, out.messages[m])
Enum(e) => full_name(out, out.enums[e])
Scalar(_) => ""
}
}
///|
/// `Outer` out of `Outer.Inner`, or `None` at the top of the file.
fn parent_of(proto : String) -> String? {
match proto.rev_find(".") {
Some(i) => Some(proto.exact_view(end=i).to_owned())
None => None
}
}
///|
fn simple_of(proto : String) -> String {
match proto.rev_find(".") {
Some(i) => proto.exact_view(start=i + 1).to_owned()
None => proto
}
}
///|
/// A field number given as text, checked against the protobuf range.
fn number_of(text : String, where_ : String) -> Int32 raise {
let n = @string.parse_int(text) catch {
_ => fail("\{where_}: `number=\{quoted(text)}` is not a number")
}
if n < 1 || n > 536870911 {
fail("\{where_}: field number \{n} is outside 1 to 536870911")
}
if n >= 19000 && n <= 19999 {
fail("\{where_}: field numbers 19000 to 19999 are reserved by protobuf")
}
n
}
///|
/// The `FieldDescriptor::new(...)` call for one field.
fn field_desc(
name : String,
number : Int32,
base : Base,
type_name : String,
extra : Array[String],
) -> String {
let parts = [quoted(name), "\{number}", field_type(base)]
if type_name != "" {
parts.push("type_name=\{quoted(type_name)}")
}
parts.append(extra)
"@moonrpc.FieldDescriptor::new(\{parts.join(", ")})"
}
///|
/// A message's codec and descriptor.
fn message(
out : Out,
name : String,
fields : Array[@gen.Field],
public : Bool,
where_ : String,
file : String,
) -> Unit raise {
let proto_name = out.messages[name]
let writes = []
let locals = []
let arms = []
let builds = []
let described : Described = { fields: [], entries: [], oneofs: [], }
let numbers : Map[Int32, String] = Map([])
let mut next = 1
for field in fields {
let attr = single(
rpc_attrs(field.attrs, file),
["rpc.field", "rpc.skip"],
"a field",
file,
)
let at_field = match attr {
Some(a) => at(a, file)
None => where_
}
if attr is Some(a) && @attr.name_of(a) is Some("rpc.skip") {
ignore(skip_spec.bind(a))
builds.push("\{field.name}: Default::default()")
continue
}
let args = match attr {
Some(a) => Some(field_spec.bind(a))
None => None
}
let n = match args {
Some(a) =>
match a.str("number") {
Some(t) => number_of(t, at_field)
None => next
}
None => next
}
if numbers.get(n) is Some(other) {
fail(
"\{at_field}: field `\{field.name}` takes number \{n}, already `\{other}`'s",
)
}
numbers.set(n, field.name)
next = n + 1
guard field_of(field.ty, out.messages, out.enums)
is Some((base, card, elem)) else {
fail(
"\{at_field}: field `\{field.name}`: `\{field.ty}` has no protobuf type",
)
}
let proto = match args {
Some(a) => a.str("name").unwrap_or(field.name)
None => field.name
}
let f = "self.\{field.name}"
let v = "v_\{field.name}"
let read = read_call(base, "r")
let wire = wire_of(base)
let type_name = composite_name(out, base)
match card {
Single => {
writes.push(
match nonzero(base, f) {
Some(cond) => "if \{cond} { \{write_call(base, n, f)} }"
None => write_call(base, n, f)
},
)
locals.push("let \{v} : Ref[\{elem}] = Ref(\{zero_of(base)})")
arms.push("(\{n}, \{wire}) => \{v}.val = \{read}")
described.fields.push(field_desc(proto, n, base, type_name, []))
}
Optional => {
writes.push("if \{f} is Some(x) { \{write_call(base, n, "x")} }")
locals.push("let \{v} : Ref[\{elem}?] = Ref(None)")
arms.push("(\{n}, \{wire}) => \{v}.val = Some(\{read})")
described.fields.push(
field_desc(proto, n, base, type_name, [
"oneof_index=\{described.oneofs.length()}",
"proto3_optional=true",
]),
)
described.oneofs.push(quoted("_" + proto))
}
Repeated => {
if packed(base) {
writes.push(
(
$|if !\{f}.is_empty() {
$| let p = @moonrpc.PbWriter::new()
$| for x in \{f} { \{packed_write(base, "x")} }
$| w.write_tag(\{n}, LengthDelim)
$| w.write_len_delim(p.to_bytes())
$|}
),
)
arms.push(
"(\{n}, LengthDelim) => { let sub = @moonrpc.PbReader::new(r.read_len_delim()); while !sub.eof() { \{v}.push(\{read_call(base, "sub")}) } }",
)
} else {
writes.push("for x in \{f} { \{write_call(base, n, "x")} }")
}
locals.push("let \{v} : Array[\{elem}] = []")
arms.push("(\{n}, \{wire}) => \{v}.push(\{read})")
described.fields.push(
field_desc(proto, n, base, type_name, ["label=LabelRepeated"]),
)
}
Keyed(key, key_ty) => {
let k = Base::Scalar(key)
// Both halves of an entry are written even when zero, as protoc's
// runtimes write them.
writes.push(
(
$|for k, x in \{f} {
$| let entry = {
$| let w = @moonrpc.PbWriter::new()
$| \{write_call(k, 1, "k")}
$| \{write_call(base, 2, "x")}
$| w.to_bytes()
$| }
$| w.message_(\{n}, entry)
$|}
),
)
locals.push("let \{v} : Map[\{key_ty}, \{elem}] = Map([])")
arms.push(
"(\{n}, LengthDelim) => { let e = @moonrpc.PbReader::new(r.read_len_delim()); let k : Ref[\{key_ty}] = Ref(\{zero_of(k)}); let x : Ref[\{elem}] = Ref(\{zero_of(base)}); while !e.eof() { match e.read_tag() { (1, \{wire_of(k)}) => k.val = \{read_call(k, "e")}; (2, \{wire}) => x.val = \{read_call(base, "e")}; (_, wire) => e.skip(wire) } }; \{v}[k.val] = x.val }",
)
let entry = pascal(proto) + "Entry"
described.entries.push(
"@moonrpc.MessageDescriptor::new(\{quoted(entry)}, [\{field_desc("key", 1, k, "", [])}, \{field_desc("value", 2, base, type_name, [])}], map_entry=true)",
)
described.fields.push(
field_desc(
proto,
n,
Message(""),
full_name(out, proto_name) + "." + entry,
["label=LabelRepeated"],
),
)
}
}
// `quote` renders through moonbitlang/parser's printer, which drops `mut`
// from `let mut`, so the single fields are held in `Ref`s.
builds.push(
if card is (Repeated | Keyed(_, _)) {
"\{field.name}: \{v}"
} else {
"\{field.name}: \{v}.val"
},
)
}
arms.push("(_, wire) => r.skip(wire)")
let vis = if public { "pub " } else { "" }
out.impls.push(
(
$|\{vis}impl @moonrpc.Message for \{name} with fn encode(self) {
$| let w = @moonrpc.PbWriter::new()
$| \{writes.join("\n")}
$| w.to_bytes()
$|}
$|
$|\{vis}impl @moonrpc.Message for \{name} with fn decode(b) {
$| let r = @moonrpc.PbReader::new(b)
$| \{locals.join("\n")}
$| while !r.eof() {
$| match r.read_tag() {
$| \{arms.join("\n")}
$| }
$| }
$| \{name}::{ \{builds.join(", ")} }
$|}
),
)
out.described[proto_name] = described
}
///|
/// The number `#rpc.value(number=...)` or a MoonBit tag gives a constructor.
fn enum_number(text : String, where_ : String) -> Int32 raise {
@string.parse_int(text) catch {
_ => fail("\{where_}: `\{text}` is not an enum number")
}
}
///|
/// An enum's `Enum` and descriptor. A constructor without payload is a value;
/// one holding an `Int` takes the numbers the enum does not know.
fn enumeration(
out : Out,
name : String,
raw : @syntax.Impl,
public : Bool,
where_ : String,
file : String,
) -> Unit raise {
let proto_name = out.enums[name]
guard raw is TopTypeDef({ components: Variant(constrs), .. }) else {
fail("\{where_}: `#rpc.enum` goes on an enum")
}
let values = []
let mut carrier = None
let numbers : Map[Int32, String] = Map([])
let mut next = 0
for c in constrs {
let ctor = c.name.name
let attr = single(
rpc_attrs(c.attrs, file),
["rpc.value"],
"a constructor",
file,
)
if c.args is Some(params) {
guard params.to_array()
is [
{
ty: Name(constr_id={ id: Ident(name="Int" | "Int32"), .. }, ..),
..
},
] &&
attr is None &&
carrier is None else {
fail(
"\{where_}: `\{name}::\{ctor}` carries a payload; one constructor may, holding an `Int` for the numbers the enum does not know",
)
}
carrier = Some(ctor)
continue
}
let here = match attr {
Some(a) => at(a, file)
None => where_
}
let args = match attr {
Some(a) => Some(value_spec.bind(a))
None => None
}
let given = match args.bind(a => a.str("number")) {
Some(t) => Some(enum_number(t, here))
None => None
}
let tagged = match c.tag {
Some((t, _)) => Some(enum_number(t, here))
None => None
}
let n = match (tagged, given) {
(Some(t), Some(g)) if t != g =>
fail("\{here}: `\{ctor}` is tagged \{t} but numbered \{g}")
(Some(t), _) => t
(None, Some(g)) => g
(None, None) => next
}
if values.is_empty() && n != 0 {
fail("\{here}: a proto3 enum's first value is 0; `\{ctor}` is \{n}")
}
if numbers.get(n) is Some(other) {
fail("\{here}: `\{ctor}` takes number \{n}, already `\{other}`'s")
}
numbers[n] = ctor
next = n + 1
let value_name = args
.bind(a => a.str("name"))
.unwrap_or(upper_snake(simple_of(proto_name)) + "_" + upper_snake(ctor))
values.push((ctor, value_name, n))
}
guard values is [(first, _, _), ..] else {
fail("\{where_}: `\{name}` has no value")
}
let codes = values.map(v => "\{v.0} => \{v.2}")
let ofs = values.map(v => "\{v.2} => \{v.0}")
match carrier {
Some(k) => {
codes.push("\{k}(n) => n")
ofs.push("n => \{k}(n)")
}
None => ofs.push("_ => \{first}")
}
let vis = if public { "pub " } else { "" }
out.impls.push(
(
$|\{vis}impl @moonrpc.Enum for \{name} with fn code(self) {
$| match self {
$| \{codes.join("\n")}
$| }
$|}
$|
$|\{vis}impl @moonrpc.Enum for \{name} with fn of(n) {
$| match n {
$| \{ofs.join("\n")}
$| }
$|}
),
)
let described = values.map(v => "{ name: \{quoted(v.1)}, number: \{v.2} }")
out.enum_descs[proto_name] = "@moonrpc.EnumDescriptor::{ name: \{quoted(simple_of(proto_name))}, values: [\{described.join(", ")}] }"
}
///|
fn is_context(ty : String) -> Bool {
ty is ("RpcContext" | "@moonrpc.RpcContext" | "@stream.RpcContext")
}
///|
/// One method of a service: its registration, its descriptor, and its call on
/// the service's client.
fn rpc_method(
out : Out,
service : Service,
owner : String,
attr : @attribute.Attribute,
name : String,
params : ArrayView[@gen.Param],
returns : String?,
file : String,
) -> Unit raise {
let where_ = at(attr, file)
let args = method_spec.bind(attr)
let rpc_name = args.str("name").unwrap_or(pascal(name))
guard params is [{ name: Some("self"), .. }, .. rest] else {
fail("\{where_}: an RPC method takes `self` first")
}
let call = ["self"]
let mut injected = false
let mut request = None
for param in rest {
guard param.name is Some(p) &&
param.ty is Some(ty) &&
param.pass is Positional else {
fail(
"\{where_}: parameters after `self` are positional, with a name and a type",
)
}
if is_context(ty) {
injected = true
call.push("ctx")
} else if request is None {
request = Some((p, ty))
call.push("req")
} else {
fail("\{where_}: an RPC method takes one request, `\{p}` is a second")
}
}
guard request is Some((req_name, req_ty)) else {
fail("\{where_}: an RPC method takes a request message")
}
guard field_of(req_ty, out.messages, out.enums)
is Some((Message(req), req_card, _)) &&
req_card is (Single | Repeated) else {
fail(
"\{where_}: the request `\{req_ty}` is not a `#rpc.message` or an array of one",
)
}
guard returns is Some(rep_ty) &&
field_of(rep_ty, out.messages, out.enums)
is Some((Message(rep), rep_card, _)) &&
rep_card is (Single | Repeated) else {
fail("\{where_}: the reply is not a `#rpc.message` or an array of one")
}
let client_streaming = req_card == Repeated
let server_streaming = rep_card == Repeated
let builder = match (client_streaming, server_streaming) {
(false, false) => "unary"
(false, true) => "server_streaming"
(true, false) => "client_streaming"
(true, true) => "bidi"
}
let path = "/\{service.name}/\{rpc_name}"
let ctx = if injected { "ctx" } else { "_ctx" }
service.register.push(
"server.register_handler(\{quoted(path)}, @moonrpc.\{builder}((\{ctx}, req : \{req_ty}) => \{owner}::\{name}(\{call.join(", ")})))",
)
service.methods.push(
"{ name: \{quoted(rpc_name)}, input_type: \{quoted(full_name(out, out.messages[req]))}, output_type: \{quoted(full_name(out, out.messages[rep]))}, client_streaming: \{client_streaming}, server_streaming: \{server_streaming} }",
)
let requests = if client_streaming {
"\{req_name}.map(m => @moonrpc.Message::encode(m))"
} else {
"[@moonrpc.Message::encode(\{req_name})]"
}
let take = if server_streaming { "replies" } else { "reply" }
let vis = if service.public { "pub " } else { "" }
service.calls.push(
(
$|\{vis}async fn[C : @moonrpc.Caller] \{owner}Client::\{name}(
$| self : \{owner}Client[C],
$| \{req_name} : \{req_ty},
$| metadata? : Array[@moonrpc.Header],
$| timeout_millis? : Int32,
$| retry? : @moonrpc.Retry,
$|) -> \{rep_ty} {
$| self.channel.invoke(\{quoted(path)}, \{requests}, metadata.unwrap_or([]), timeout_millis, retry).\{take}()
$|}
),
)
}
///|
/// First pass: the messages, enums and services, and the package they share.
/// A dotted `name=` must nest in a message of the same file.
fn collect(decls : Array[@gen.Decl], out : Out) -> Unit raise {
let places : Map[String, String] = Map([])
let claim = (proto : String, where_ : String) => {
if places.get(proto) is Some(first) {
fail("\{where_}: `\{proto}` is declared already, at \{first}")
}
places[proto] = where_
}
for decl in decls {
match decl {
Struct(name~, attrs~, public~, file~, ..) => {
guard single(
rpc_attrs(attrs, file),
["rpc.message", "rpc.service"],
"a struct",
file,
)
is Some(attr) else {
continue
}
if @attr.name_of(attr) is Some("rpc.message") {
let proto = message_spec.bind(attr).str("name").unwrap_or(name)
claim(proto, at(attr, file))
out.messages.set(name, proto)
out.message_order.push(proto)
} else {
let full = service_spec.bind(attr).str("name").unwrap_or(name)
let pkg = parent_of(full).unwrap_or("")
match out.pkg {
Some(p) if p != pkg =>
fail(
"\{at(attr, file)}: services in one file share a package; `\{pkg}` differs from `\{p}`",
)
_ => out.pkg = Some(pkg)
}
out.services.set(name, {
name: full,
public,
register: [],
methods: [],
calls: [],
})
out.order.push(name)
}
}
Enum(name~, attrs~, file~, ..) => {
guard single(rpc_attrs(attrs, file), ["rpc.enum"], "an enum", file)
is Some(attr) else {
continue
}
let proto = enum_spec.bind(attr).str("name").unwrap_or(name)
claim(proto, at(attr, file))
out.enums.set(name, proto)
out.enum_order.push(proto)
}
_ => ()
}
}
for proto, where_ in places {
if parent_of(proto) is Some(parent) && !out.message_order.contains(parent) {
fail(
"\{where_}: `\{proto}` nests in `\{parent}`, which is no `#rpc.message` here",
)
}
}
}
///|
/// A message's descriptor, with the map entries, messages and enums nested in
/// it.
fn described(out : Out, proto : String) -> String {
let d = out.described[proto]
let nested = d.entries.copy()
for m in out.message_order {
if parent_of(m) == Some(proto) {
nested.push(described(out, m))
}
}
let enums = []
for e in out.enum_order {
if parent_of(e) == Some(proto) {
enums.push(out.enum_descs[e])
}
}
let parts = [quoted(simple_of(proto)), "[\{d.fields.join(", ")}]"]
if !nested.is_empty() {
parts.push("nested=[\{nested.join(", ")}]")
}
if !enums.is_empty() {
parts.push("enums=[\{enums.join(", ")}]")
}
if !d.oneofs.is_empty() {
parts.push("oneofs=[\{d.oneofs.join(", ")}]")
}
"@moonrpc.MessageDescriptor::new(\{parts.join(", ")})"
}
///|
impl @gen.Generator for Rpc with fn generate(_, decls) {
let out : Out = {
messages: Map([]),
enums: Map([]),
services: Map([]),
order: [],
pkg: None,
impls: [],
described: Map([]),
message_order: [],
enum_descs: Map([]),
enum_order: [],
}
collect(decls, out)
let mut source_file = "rpc.mbt"
for decl in decls {
match decl {
Struct(name~, fields~, attrs~, public~, file~, ..) =>
if out.messages.contains(name) {
source_file = file
let attr = rpc_attrs(attrs, file)[0]
message(out, name, fields, public, at(attr, file), file)
} else if out.services.contains(name) {
source_file = file
} else {
for field in fields {
if rpc_attrs(field.attrs, file) is [first, ..] {
fail(
"\{at(first, file)}: `\{name}` has no `#rpc.message`, so its fields take no `#rpc.*`",
)
}
}
}
Func(owner~, name~, params~, returns~, attrs~, file~, ..) => {
guard single(rpc_attrs(attrs, file), ["rpc.method"], "a function", file)
is Some(attr) else {
continue
}
guard owner is Some(ty) && out.services.get(ty) is Some(service) else {
fail(
"\{at(attr, file)}: `#rpc.method` goes on a method of a `#rpc.service`",
)
}
rpc_method(out, service, ty, attr, name, params, returns, file)
}
Enum(name~, attrs~, public~, file~, raw~, ..) =>
if out.enums.contains(name) {
source_file = file
let attr = rpc_attrs(attrs, file)[0]
enumeration(out, name, raw, public, at(attr, file), file)
} else if raw is TopTypeDef({ components: Variant(constrs), .. }) {
for c in constrs {
if rpc_attrs(c.attrs, file) is [first, ..] {
fail(
"\{at(first, file)}: `\{name}` has no `#rpc.enum`, so its constructors take no `#rpc.*`",
)
}
}
}
Other(..) => ()
}
}
if out.messages.is_empty() && out.enums.is_empty() && out.services.is_empty() {
return @quote.join([])
}
let items = []
let services = []
for ty in out.order {
let s = out.services[ty]
let vis = if s.public { "pub " } else { "" }
items.push(
(
$|\{vis}fn[R : @moonrpc.Registrar] \{ty}::register(self : \{ty}, server : R) -> Unit {
$| \{s.register.join("\n")}
$|}
$|
$|\{vis}struct \{ty}Client[C] {
$| channel : C
$|}
$|
$|\{vis}fn[C] \{ty}Client::new(channel : C) -> \{ty}Client[C] {
$| { channel, }
$|}
),
)
items.append(s.calls)
services.push(
"{ name: \{quoted(simple_of(s.name))}, methods: [\{s.methods.join(", ")}] }",
)
}
let stem = source_file
.exact_view(
end=if source_file.has_suffix(".mbt") {
source_file.length() - 4
} else {
source_file.length()
},
)
.to_owned()
let stem = match stem.rev_find("/") {
Some(i) => stem.exact_view(start=i + 1).to_owned()
None => stem
}
let messages = out.message_order
.filter(m => parent_of(m) is None)
.map(m => described(out, m))
let enums = out.enum_order
.filter(e => parent_of(e) is None)
.map(e => out.enum_descs[e])
let parts = [quoted(stem + ".proto"), quoted(out.pkg.unwrap_or(""))]
if !messages.is_empty() {
parts.push("messages=[\{messages.join(", ")}]")
}
if !enums.is_empty() {
parts.push("enums=[\{enums.join(", ")}]")
}
if !services.is_empty() {
parts.push("services=[\{services.join(", ")}]")
}
items.push(
(
$|pub fn descriptor() -> @moonrpc.FileDescriptor {
$| @moonrpc.FileDescriptor::new(\{parts.join(", ")})
$|}
),
)
items.append(out.impls)
@quote.items(items.join("\n\n")).render()
}