///|
/// Convert an identifier to `snake_case` (← protobuf's PascalCase RPC/service
/// names, which must become MoonBit value names). An uppercase letter starts a
/// new word when it follows a lowercase letter or digit, or when it is followed
/// by a lowercase letter (so `HTTPServer` → `http_server`, `SayHello` →
/// `say_hello`). Existing underscores are preserved and the result is lowercased.
fn to_snake(s : String) -> String {
let n = s.length()
let mut r = ""
for i = 0; i < n; i = i + 1 {
let c = s[i].to_int()
let is_upper = c >= 0x41 && c <= 0x5A
if is_upper && i > 0 {
let prev = s[i - 1].to_int()
let prev_lower_or_digit = (prev >= 0x61 && prev <= 0x7A) ||
(prev >= 0x30 && prev <= 0x39)
let next_lower = i + 1 < n &&
({
let nx = s[i + 1].to_int()
nx >= 0x61 && nx <= 0x7A
})
if prev_lower_or_digit || next_lower {
r = r + "_"
}
}
r = r + s[i:i + 1].to_owned()
}
r.to_lower()
}
///|
/// Join `parts` with `", "` — the codegen workhorse for column/argument lists.
fn commas(parts : Array[String]) -> String {
let mut out = ""
for i = 0; i < parts.length(); i = i + 1 {
if i > 0 {
out = out + ", "
}
out = out + parts[i]
}
out
}
///|
/// Map a protobuf scalar type onto its MoonBit spelling (proto3 scalar set). An
/// unrecognised name is a message/enum reference; a qualified reference
/// (`Outer.Inner`, `pkg.Message`) resolves to its last segment — the simple name
/// nested types are hoisted under and top-level types are generated as.
fn map_proto_type(raw : String) -> String {
match raw {
"double" => "Double"
"float" => "Float"
"int32" | "sint32" | "sfixed32" => "Int"
"int64" | "sint64" | "sfixed64" => "Int64"
"uint32" | "fixed32" => "UInt"
"uint64" | "fixed64" => "UInt64"
"bool" => "Bool"
"string" => "String"
"bytes" => "Bytes"
_ => last_segment(raw)
}
}
///|
/// The part of a dotted name after its last `.` (`Outer.Inner` -> `Inner`); the
/// whole string when there is no `.`.
fn last_segment(s : String) -> String {
let mut idx = -1
for i = 0; i < s.length(); i = i + 1 {
if s[i] == '.' {
idx = i
}
}
if idx >= 0 {
s[idx + 1:].to_owned()
} else {
s
}
}
///|
/// One field of a protobuf `message`: its name, the (already MoonBit-mapped)
/// type, and its wire number (the `= N`). `repeated` becomes `Array[T]` and
/// `map` becomes `Map[K, V]`.
pub(all) struct ProtoField {
name : String
type_ : String
number : Int
}
///|
/// A protobuf `oneof` group: its name and the members (exactly one may be set).
/// Generated into a MoonBit enum — the faithful equivalent of the "exactly one"
/// invariant, which flattening to optional fields would lose.
pub(all) struct ProtoOneof {
name : String
variants : Array[ProtoField]
}
///|
/// A protobuf `message` declaration, generated into a MoonBit struct. `oneof`
/// groups are lifted out of `fields` into `oneofs`; `reserved` field numbers and
/// names are recorded so a reused one can be caught.
pub(all) struct ProtoMessage {
name : String
fields : Array[ProtoField]
oneofs : Array[ProtoOneof]
reserved_numbers : Array[Int]
reserved_names : Array[String]
}
///|
/// Capitalise the first letter (a protobuf `field_name` becomes a MoonBit enum
/// variant `Field_name`, which must start uppercase).
fn cap_first(s : String) -> String {
if s.length() == 0 {
return s
}
let c = s[0].to_int()
if c >= 0x61 && c <= 0x7A {
(c - 0x20).to_byte().to_char().to_string() + s[1:].to_owned()
} else {
s
}
}
///|
/// One value of a protobuf `enum`: its name and its integer number.
pub(all) struct ProtoEnumValue {
name : String
number : Int
}
///|
/// A protobuf `enum` declaration, generated into a MoonBit enum plus an integer
/// mapping (proto3 enums are int-backed, and the value numbered `0` is the
/// default).
pub(all) struct ProtoEnum {
name : String
values : Array[ProtoEnumValue]
}
///|
/// One `rpc` method of a service: its name, request and response message names,
/// and whether either side is a `stream` (client/server/bidi streaming).
pub(all) struct Rpc {
name : String
request : String
response : String
client_streaming : Bool
server_streaming : Bool
}
///|
/// A protobuf `service` declaration and its RPC methods.
pub(all) struct ProtoService {
name : String
rpcs : Array[Rpc]
}
///|
/// A parsed `.proto` file: its `package` (empty when none) and the services and
/// messages it declares.
pub(all) struct Proto {
package_ : String
services : Array[ProtoService]
messages : Array[ProtoMessage]
enums : Array[ProtoEnum]
}
///|
/// Tokenise a `.proto` source: whitespace-separated words plus the single-char
/// punctuation `{ } ( ) < > ; = ,` as their own tokens, with `//` line comments
/// and `/* … */` block comments stripped and `"…"` string literals kept whole.
fn proto_tokens(src : String) -> Array[String] {
let out : Array[String] = []
let n = src.length()
let mut i = 0
while i < n {
let c = src[i]
if is_ws(c) {
i = i + 1
continue
}
if c == '/' && i + 1 < n && src[i + 1] == '/' {
while i < n && src[i] != '\n' {
i = i + 1
}
continue
}
if c == '/' && i + 1 < n && src[i + 1] == '*' {
i = i + 2
while i + 1 < n && !(src[i] == '*' && src[i + 1] == '/') {
i = i + 1
}
i = i + 2
continue
}
if c == '"' {
let start = i
i = i + 1
while i < n && src[i] != '"' {
i = i + 1
}
i = i + 1
out.push(src[start:i].to_owned())
continue
}
if c == '{' ||
c == '}' ||
c == '(' ||
c == ')' ||
c == '<' ||
c == '>' ||
c == ';' ||
c == '=' ||
c == ',' {
out.push(src[i:i + 1].to_owned())
i = i + 1
continue
}
let start = i
while i < n {
let d = src[i]
if is_ws(d) ||
d == '{' ||
d == '}' ||
d == '(' ||
d == ')' ||
d == '<' ||
d == '>' ||
d == ';' ||
d == '=' ||
d == ',' ||
d == '"' {
break
}
i = i + 1
}
out.push(src[start:i].to_owned())
}
out
}
///|
/// Index just past the next `;` at or after `i` (used to skip a statement).
fn skip_to_semi(toks : Array[String], i : Int) -> Int {
let n = toks.length()
let mut j = i
while j < n && toks[j] != ";" {
j = j + 1
}
j + 1
}
///|
/// Given `toks[i] == "{"`, index just past its matching `}` (brace-balanced).
fn skip_braced(toks : Array[String], i : Int) -> Int {
let n = toks.length()
let mut depth = 0
let mut j = i
while j < n {
if toks[j] == "{" {
depth = depth + 1
} else if toks[j] == "}" {
depth = depth - 1
if depth == 0 {
return j + 1
}
}
j = j + 1
}
j
}
///|
/// Parse a `message` block starting at `toks[start] == "message"`. Returns the
/// message and the index just past its closing `}`. Handles scalar/message
/// fields, `repeated`, `map`, `optional`/`required`, and lifts each `oneof`
/// group into `oneofs`. Nested `message` / `enum` declarations are hoisted into
/// `out_messages` / `out_enums` (MoonBit has no nested types), by their simple
/// names; `reserved` and `option` lines are skipped.
fn parse_message(
toks : Array[String],
start : Int,
out_messages : Array[ProtoMessage],
out_enums : Array[ProtoEnum],
) -> (ProtoMessage, Int) {
let n = toks.length()
let name = if start + 1 < n { toks[start + 1] } else { "Unnamed" }
let fields : Array[ProtoField] = []
let oneofs : Array[ProtoOneof] = []
let reserved_numbers : Array[Int] = []
let reserved_names : Array[String] = []
let mut i = start + 2
if i < n && toks[i] == "{" {
i = i + 1
}
while i < n && toks[i] != "}" {
let t = toks[i]
if t == "option" {
i = skip_to_semi(toks, i)
continue
}
if t == "reserved" {
let (nums, names, ni) = parse_reserved(toks, i)
for v in nums {
reserved_numbers.push(v)
}
for nm in names {
reserved_names.push(nm)
}
i = ni
continue
}
if t == "message" {
let (nested, ni) = parse_message(toks, i, out_messages, out_enums)
out_messages.push(nested)
i = ni
continue
}
if t == "enum" {
let (nested, ni) = parse_enum(toks, i)
out_enums.push(nested)
i = ni
continue
}
if t == "oneof" {
let oneof_name = if i + 1 < n { toks[i + 1] } else { "oneof" }
i = i + 2
if i < n && toks[i] == "{" {
i = i + 1
}
let variants : Array[ProtoField] = []
while i < n && toks[i] != "}" {
let (f, ni) = parse_field(toks, i)
match f {
Some(field) => variants.push(field)
None => ()
}
i = ni
}
i = i + 1
oneofs.push({ name: oneof_name, variants, })
continue
}
let (f, ni) = parse_field(toks, i)
match f {
Some(field) => fields.push(field)
None => ()
}
i = ni
}
i = i + 1
({ name, fields, oneofs, reserved_numbers, reserved_names, }, i)
}
///|
/// The names of `m`'s fields (oneof members included) that reuse a `reserved`
/// field number or name — protobuf forbids reusing either, and this surfaces an
/// accidental reuse. Empty when the message is clean.
pub fn proto_reserved_conflicts(m : ProtoMessage) -> Array[String] {
let out : Array[String] = []
fn check(f : ProtoField) -> Unit {
if m.reserved_numbers.contains(f.number) ||
m.reserved_names.contains(f.name) {
out.push(f.name)
}
}
for f in m.fields {
check(f)
}
for o in m.oneofs {
for v in o.variants {
check(v)
}
}
out
}
///|
/// Parse a single message field statement (`[repeated] Type name = N ;` or
/// `map name = N ;`) starting at `i`; returns the field (or `None` if the
/// line is not a field) and the index just past its `;`.
fn parse_field(toks : Array[String], i : Int) -> (ProtoField?, Int) {
let n = toks.length()
if i >= n {
return (None, n)
}
if toks[i] == "map" {
let mut j = i + 1
if j < n && toks[j] == "<" {
j = j + 1
}
let k = if j < n { toks[j] } else { "string" }
j = j + 1
if j < n && toks[j] == "," {
j = j + 1
}
let v = if j < n { toks[j] } else { "string" }
j = j + 1
if j < n && toks[j] == ">" {
j = j + 1
}
let fname = if j < n { toks[j] } else { "field" }
let number = if j + 2 < n && toks[j + 1] == "=" {
proto_int(toks[j + 2])
} else {
0
}
let end = skip_to_semi(toks, j)
let type_ = "Map[" + map_proto_type(k) + ", " + map_proto_type(v) + "]"
return (Some({ name: fname, type_, number, }), end)
}
let mut j = i
let mut repeated = false
if toks[j] == "repeated" {
repeated = true
j = j + 1
} else if toks[j] == "optional" || toks[j] == "required" {
j = j + 1
}
if j + 1 >= n {
return (None, skip_to_semi(toks, i))
}
let ptype = toks[j]
let fname = toks[j + 1]
let number = if j + 3 < n && toks[j + 2] == "=" {
proto_int(toks[j + 3])
} else {
0
}
let end = skip_to_semi(toks, j + 1)
let mapped = map_proto_type(ptype)
let type_ = if repeated { "Array[" + mapped + "]" } else { mapped }
(Some({ name: fname, type_, number, }), end)
}
///|
/// Parse a `service` block starting at `toks[start] == "service"`. Returns the
/// service and the index just past its closing `}`.
fn parse_service(toks : Array[String], start : Int) -> (ProtoService, Int) {
let n = toks.length()
let name = if start + 1 < n { toks[start + 1] } else { "Unnamed" }
let rpcs : Array[Rpc] = []
let mut i = start + 2
if i < n && toks[i] == "{" {
i = i + 1
}
while i < n && toks[i] != "}" {
if toks[i] == "rpc" {
let mname = if i + 1 < n { toks[i + 1] } else { "Unnamed" }
let mut j = i + 2
if j < n && toks[j] == "(" {
j = j + 1
}
let mut client_streaming = false
if j < n && toks[j] == "stream" {
client_streaming = true
j = j + 1
}
let request = if j < n { toks[j] } else { "Unit" }
j = j + 1
if j < n && toks[j] == ")" {
j = j + 1
}
if j < n && toks[j] == "returns" {
j = j + 1
}
if j < n && toks[j] == "(" {
j = j + 1
}
let mut server_streaming = false
if j < n && toks[j] == "stream" {
server_streaming = true
j = j + 1
}
let response = if j < n { toks[j] } else { "Unit" }
j = j + 1
if j < n && toks[j] == ")" {
j = j + 1
}
rpcs.push({
name: mname,
request,
response,
client_streaming,
server_streaming,
})
i = if j < n && toks[j] == "{" {
skip_braced(toks, j)
} else {
skip_to_semi(toks, j)
}
} else if toks[i] == "option" {
i = skip_to_semi(toks, i)
} else {
i = i + 1
}
}
i = i + 1
({ name, rpcs, }, i)
}
///|
/// Parse a leading-digit integer token (an enum value's number), stopping at the
/// first non-digit; `0` when there are no digits. Handles a leading `-`.
fn proto_int(s : String) -> Int {
let mut acc = 0
let mut neg = false
let mut k = 0
if s.length() > 0 && s[0] == '-' {
neg = true
k = 1
}
while k < s.length() {
let c = s[k].to_int()
if c < 0x30 || c > 0x39 {
break
}
acc = acc * 10 + (c - 0x30)
k = k + 1
}
if neg {
-acc
} else {
acc
}
}
///|
/// Parse an `enum` block starting at `toks[start] == "enum"`. Returns the enum and
/// the index just past its closing `}`. Each `NAME = NUMBER ;` becomes a value;
/// `option` and `reserved` lines inside the block are skipped.
fn parse_enum(toks : Array[String], start : Int) -> (ProtoEnum, Int) {
let n = toks.length()
let name = if start + 1 < n { toks[start + 1] } else { "Unnamed" }
let values : Array[ProtoEnumValue] = []
let mut i = start + 2
if i < n && toks[i] == "{" {
i = i + 1
}
while i < n && toks[i] != "}" {
let t = toks[i]
if t == "option" || t == "reserved" {
i = skip_to_semi(toks, i)
continue
}
let mut j = i + 1
if j < n && toks[j] == "=" {
j = j + 1
}
let number = if j < n { proto_int(toks[j]) } else { 0 }
values.push({ name: t, number, })
i = skip_to_semi(toks, i)
}
i = i + 1
({ name, values, }, i)
}
///|
/// Parse a `reserved` statement starting at `toks[start] == "reserved"`: a list of
/// field numbers, `N to M` ranges (expanded), and quoted field names. Returns the
/// reserved numbers, the reserved names, and the index just past the `;`.
fn parse_reserved(
toks : Array[String],
start : Int,
) -> (Array[Int], Array[String], Int) {
let n = toks.length()
let nums : Array[Int] = []
let names : Array[String] = []
let mut i = start + 1
while i < n && toks[i] != ";" {
let t = toks[i]
if t == "," {
i = i + 1
continue
}
if t.length() >= 2 && t[0] == '"' {
names.push(t[1:t.length() - 1].to_owned())
i = i + 1
continue
}
let lo = proto_int(t)
if i + 2 < n && toks[i + 1] == "to" {
let hi = proto_int(toks[i + 2])
for v = lo; v <= hi; v = v + 1 {
nums.push(v)
}
i = i + 3
} else {
nums.push(lo)
i = i + 1
}
}
(nums, names, i + 1)
}
///|
/// Parse a minimal `.proto` (proto3). Recognises the `package` declaration,
/// `service` blocks with their `rpc` methods (including `stream`), `message`
/// blocks with scalar/`repeated`/`map` fields, and top-level `enum` declarations.
/// `syntax`, `import`, and `option` are skipped.
///
/// ```
/// syntax = "proto3";
/// package greet;
/// service Greeter {
/// rpc SayHello (HelloRequest) returns (HelloReply);
/// }
/// message HelloRequest { string name = 1; }
/// message HelloReply { string message = 1; }
/// ```
pub fn parse_proto(source : String) -> Proto {
let toks = proto_tokens(source)
let n = toks.length()
let mut package_ = ""
let services : Array[ProtoService] = []
let messages : Array[ProtoMessage] = []
let enums : Array[ProtoEnum] = []
let mut i = 0
while i < n {
let t = toks[i]
if t == "package" {
let mut name = ""
i = i + 1
while i < n && toks[i] != ";" {
name = name + toks[i]
i = i + 1
}
package_ = name
i = i + 1
} else if t == "syntax" || t == "import" || t == "option" {
i = skip_to_semi(toks, i)
} else if t == "service" {
let (svc, ni) = parse_service(toks, i)
services.push(svc)
i = ni
} else if t == "message" {
let (msg, ni) = parse_message(toks, i, messages, enums)
messages.push(msg)
i = ni
} else if t == "enum" {
let (en, ni) = parse_enum(toks, i)
enums.push(en)
i = ni
} else {
i = i + 1
}
}
{ package_, services, messages, enums, }
}
///|
/// The gRPC handler input type for an RPC (its request, or `Array[Request]` when
/// the client streams).
fn rpc_in(r : Rpc) -> String {
if r.client_streaming {
"Array[" + r.request + "]"
} else {
r.request
}
}
///|
/// The gRPC handler output type for an RPC (its reply, or `Array[Reply]` when the
/// server streams).
fn rpc_out(r : Rpc) -> String {
if r.server_streaming {
"Array[" + r.response + "]"
} else {
r.response
}
}
///|
/// Generate a moonrpc service stub from a parsed `.proto`: a MoonBit struct per
/// `message`, a `@moonrpc.Method` descriptor per RPC (its gRPC `:path` is
/// `/package.Service/Method`), a `Server` handler-registration struct
/// (one synchronous-core handler field per RPC, each returning its reply or a
/// gRPC `@moonrpc.Status`), and a `_methods()` listing. The output
/// compiles against `Lfan-ke/moonrpc`.
pub fn generate_grpc(proto : Proto) -> String {
let mut out = "// Code generated by moonctl. DO NOT EDIT.\n\n"
for e in proto.enums {
out = out +
"///|\n/// `" +
e.name +
"` enum (generated from the `.proto` `enum` block).\npub enum " +
e.name +
" {\n"
for v in e.values {
out = out + " " + v.name + "\n"
}
out = out + "} derive(Eq, Debug)\n\n"
out = out +
"///|\n/// The protobuf number for a `" +
e.name +
"` value.\npub fn " +
e.name +
"::to_int(self : " +
e.name +
") -> Int {\n match self {\n"
for v in e.values {
out = out + " " + v.name + " => " + v.number.to_string() + "\n"
}
out = out + " }\n}\n\n"
let default_name = if e.values.length() > 0 {
e.values[0].name
} else {
"Unknown"
}
out = out +
"///|\n/// The `" +
e.name +
"` value for a protobuf number, defaulting to the zero value.\npub fn " +
e.name +
"::from_int(n : Int) -> " +
e.name +
" {\n match n {\n"
for v in e.values {
out = out + " " + v.number.to_string() + " => " + v.name + "\n"
}
out = out + " _ => " + default_name + "\n }\n}\n\n"
}
for m in proto.messages {
// A oneof becomes a union enum (exactly one variant set), defined before the
// struct that carries it.
for o in m.oneofs {
let ename = m.name + cap_first(o.name)
out = out +
"///|\n/// `" +
m.name +
"." +
o.name +
"` oneof: exactly one variant is set.\npub enum " +
ename +
" {\n"
for v in o.variants {
out = out + " " + cap_first(v.name) + "(" + v.type_ + ")\n"
}
out = out + "} derive(Eq, Debug)\n\n"
}
out = out +
"///|\n/// `" +
m.name +
"` message (generated from the `.proto` `message` block).\npub(all) struct " +
m.name +
" {\n"
for f in m.fields {
out = out + " " + f.name + " : " + f.type_ + "\n"
}
for o in m.oneofs {
out = out + " " + o.name + " : " + m.name + cap_first(o.name) + "\n"
}
out = out + "}\n\n"
// The wire-number table: field name -> its protobuf number (oneof members
// included), for wire encoding and reflection.
let pairs : Array[String] = []
for f in m.fields {
pairs.push("(" + quote(f.name) + ", " + f.number.to_string() + ")")
}
for o in m.oneofs {
for v in o.variants {
pairs.push("(" + quote(v.name) + ", " + v.number.to_string() + ")")
}
}
out = out +
"///|\n/// The protobuf wire numbers of `" +
m.name +
"`'s fields.\npub fn " +
to_snake(m.name) +
"_field_numbers() -> Array[(String, Int)] {\n [" +
commas(pairs) +
"]\n}\n\n"
}
for s in proto.services {
let full = if proto.package_ == "" {
s.name
} else {
proto.package_ + "." + s.name
}
let svc = to_snake(s.name)
for r in s.rpcs {
let dname = svc + "_" + to_snake(r.name)
out = out +
"///|\n/// gRPC method descriptor for `" +
full +
"/" +
r.name +
"` (`:path` = " +
quote("/" + full + "/" + r.name) +
").\npub let " +
dname +
" : @moonrpc.Method = { service: " +
quote(full) +
", name: " +
quote(r.name) +
" }\n\n"
}
out = out +
"///|\n/// The `" +
full +
"` service: one handler per RPC (synchronous core). Each handler takes its\n/// request (an `Array` when the client streams) and returns its reply (an\n/// `Array` when the server streams) or a gRPC `@moonrpc.Status` on failure.\npub(all) struct " +
s.name +
"Server {\n"
for r in s.rpcs {
out = out +
" " +
to_snake(r.name) +
" : (" +
rpc_in(r) +
") -> Result[" +
rpc_out(r) +
", @moonrpc.Status]\n"
}
out = out + "}\n\n"
let names : Array[String] = []
for r in s.rpcs {
names.push(svc + "_" + to_snake(r.name))
}
out = out +
"///|\n/// Every method descriptor in `" +
full +
"`, in declaration order.\npub fn " +
svc +
"_methods() -> Array[@moonrpc.Method] {\n [" +
commas(names) +
"]\n}\n\n"
}
out
}