// The Thrift IDL compiler: a .thrift file in, the MoonBit types, their codecs, and
// the service's registration and client out. The subset it reads is the one this
// library can carry — struct, union, enum, exception, typedef, const and service over
// the primitive types, lists, sets and maps. Anything else is refused with the line it
// came from, rather than silently dropped.
///|
pub(all) suberror Refused {
Bad(file~ : String, line~ : Int32, msg~ : String)
}
///|
pub impl Show for Refused with fn output(self, logger) {
match self {
Bad(file~, line~, msg~) => logger.write_string("\{file}:\{line}: \{msg}")
}
}
///|
pub extend Refused with Show::{to_string, output}
///|
fn[T] refuse(file : String, line : Int32, msg : String) -> T raise Refused {
raise Bad(file~, line~, msg~)
}
// ---------------------------------------------------------------- the types
///|
/// One Thrift value type, as the IDL spells it.
pub(all) enum TType {
Bool
Byte
I16
I32
I64
Double
Str
Binary
List(TType)
Set(TType)
Map(TType, TType)
Named(String)
}
///|
pub(all) struct Field {
id : Int32
name : String
ty : TType
optional : Bool
}
///|
/// What a declared field list is. An `exception` is a struct on the wire; the
/// difference is in a service's signature, where a `throws` clause names one.
pub(all) enum DeclKind {
PlainStruct
ExceptionDecl
UnionDecl
}
///|
pub(all) struct Struct {
name : String
fields : Array[Field]
kind : DeclKind
}
///|
pub(all) struct Enum {
name : String
values : Array[(String, Int32)]
}
///|
pub(all) struct Param {
id : Int32
name : String
ty : TType
}
///|
pub(all) struct Function {
name : String
ret : TType?
params : Array[Param]
// The `throws` clause: the exceptions this method may answer instead of a value.
exceptions : Array[Param]
}
///|
pub(all) struct Service {
name : String
base : String?
functions : Array[Function]
}
///|
/// Another name for a type. It is substituted where it is used, so no alias reaches
/// the generated code — the wire does not carry one either.
pub(all) struct Typedef {
name : String
ty : TType
}
///|
/// A value written in the IDL. `CRecord` is not a separate case from `CMap`: `{ ... }`
/// is the same syntax for a map and for a struct, and the declared type decides which of
/// the two it is.
pub(all) enum ConstValue {
CBool(Bool)
CInt(Int64)
CFloat(Double)
CStr(String)
CList(Array[ConstValue])
CMap(Array[(ConstValue, ConstValue)])
// `Mood.Happy`: the enum it belongs to, and the value.
CEnum(String, String)
}
///|
pub(all) struct Constant {
name : String
ty : TType
value : ConstValue
line : Int32
}
///|
pub(all) struct Document {
structs : Array[Struct]
enums : Array[Enum]
services : Array[Service]
typedefs : Array[Typedef]
constants : Array[Constant]
// The files this one includes, as written.
includes : Array[String]
}
// ---------------------------------------------------------------- the lexer
///|
pub(all) enum Token {
TIdent(String)
TStr(String)
// A number's text, so a 64-bit constant is not squeezed into a field id's width.
TNum(String)
TFloat(Double)
TSym(Char)
TEof
}
///|
fn is_ident_start(c : Char) -> Bool {
c.is_ascii_alphabetic() || c == '_'
}
///|
fn is_ident_body(c : Char) -> Bool {
c.is_ascii_alphabetic() || c.is_ascii_digit() || c == '_' || c == '.'
}
///|
fn is_hex(c : Char) -> Bool {
c.is_ascii_digit() || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
}
///|
fn lex(file : String, source : String) -> Array[(Token, Int32)] raise Refused {
let out : Array[(Token, Int32)] = []
let n = source.length()
let mut i = 0
let mut line = 1
while i < n {
let c = source.get_char(i).unwrap_or(' ')
let next = source.get_char(i + 1)
if c == '\n' {
line = line + 1
i = i + 1
} else if c.is_ascii_whitespace() {
i = i + 1
} else if c == '/' && next == Some('/') {
while i < n && source.get_char(i) != Some('\n') {
i = i + 1
}
} else if c == '#' {
while i < n && source.get_char(i) != Some('\n') {
i = i + 1
}
} else if c == '/' && next == Some('*') {
i = i + 2
while i < n &&
!(source.get_char(i) == Some('*') &&
source.get_char(i + 1) == Some('/')) {
if source.get_char(i) == Some('\n') {
line = line + 1
}
i = i + 1
}
i = i + 2
} else if is_ident_start(c) {
let start = i
while i < n && source.get_char(i) is Some(d) && is_ident_body(d) {
i = i + 1
}
out.push((TIdent(source[start:i].to_owned()), line))
} else if c.is_ascii_digit() || c == '-' {
let start = i
if c == '-' {
i = i + 1
}
if source.get_char(i) == Some('0') &&
(
source.get_char(i + 1) == Some('x') ||
source.get_char(i + 1) == Some('X')
) {
i = i + 2
while i < n && source.get_char(i) is Some(d) && is_hex(d) {
i = i + 1
}
out.push((TNum(source[start:i].to_owned()), line))
} else {
while i < n && source.get_char(i) is Some(d) && d.is_ascii_digit() {
i = i + 1
}
let mut float = false
if source.get_char(i) == Some('.') &&
source.get_char(i + 1) is Some(d) &&
d.is_ascii_digit() {
float = true
i = i + 1
while i < n && source.get_char(i) is Some(d) && d.is_ascii_digit() {
i = i + 1
}
}
if source.get_char(i) == Some('e') || source.get_char(i) == Some('E') {
float = true
i = i + 1
if source.get_char(i) == Some('+') || source.get_char(i) == Some('-') {
i = i + 1
}
while i < n && source.get_char(i) is Some(d) && d.is_ascii_digit() {
i = i + 1
}
}
let text = source[start:i].to_owned()
if float {
let value = @string.parse_double(text[:]) catch {
_ => refuse(file, line, "`\{text}` is not a number")
}
out.push((TFloat(value), line))
} else {
out.push((TNum(text), line))
}
}
} else if c == '"' || c == '\'' {
let quote = c
let mut text = ""
i = i + 1
while i < n && source.get_char(i) != Some(quote) {
let ch = source.get_char(i).unwrap_or(' ')
if ch == '\\' {
// Keep the escapes a constant will need back: the renderer writes its own.
let escaped = source.get_char(i + 1).unwrap_or(' ')
text = text +
(match escaped {
'n' => "\n"
't' => "\t"
'r' => "\r"
_ => escaped.to_string()
})
i = i + 2
} else {
text = text + ch.to_string()
i = i + 1
}
}
i = i + 1
out.push((TStr(text), line))
} else if c == '<' || c == '>' {
out.push((TSym(c), line))
i = i + 1
} else if c.is_ascii_graphic() {
out.push((TSym(c), line))
i = i + 1
} else {
refuse(file, line, "unexpected character in the file")
}
}
out.push((TEof, line))
out
}
// ---------------------------------------------------------------- the parser
///|
struct Parser {
file : String
tokens : Array[(Token, Int32)]
mut pos : Int32
}
///|
fn Parser::peek(self : Parser) -> Token {
self.tokens[self.pos].0
}
///|
fn Parser::line(self : Parser) -> Int32 {
self.tokens[self.pos].1
}
///|
fn Parser::at_end(self : Parser) -> Bool {
self.peek() is TEof
}
///|
fn Parser::advance(self : Parser) -> Token {
let token = self.peek()
if self.pos < self.tokens.length() - 1 {
self.pos = self.pos + 1
}
token
}
///|
fn Parser::eat(self : Parser, symbol : Char) -> Bool {
if self.peek() is TSym(c) && c == symbol {
ignore(self.advance())
true
} else {
false
}
}
///|
fn Parser::expect(self : Parser, symbol : Char) -> Unit raise Refused {
if self.eat(symbol) {
return
}
refuse(self.file, self.line(), "expected `\{symbol}`")
}
///|
fn Parser::expect_ident(self : Parser, what : String) -> String raise Refused {
if self.peek() is TIdent(name) {
ignore(self.advance())
name
} else {
refuse(self.file, self.line(), "expected \{what}")
}
}
///|
fn Parser::expect_num(self : Parser) -> Int32 raise Refused {
if self.peek() is TNum(text) {
ignore(self.advance())
@string.parse_int(text[:]) catch {
_ => refuse(self.file, self.line(), "`\{text}` is not a 32-bit number")
}
} else {
refuse(self.file, self.line(), "expected a number")
}
}
///|
/// Skip a `( ... )` annotation without reading it: key-value hints for other
/// generators do not change the code this one writes.
fn Parser::skip_annotation(self : Parser) -> Unit raise Refused {
if !self.eat('(') {
return
}
let mut depth = 1
while depth > 0 {
match self.advance() {
TSym(c) =>
if c == '(' {
depth = depth + 1
} else if c == ')' {
depth = depth - 1
}
TEof => refuse(self.file, self.line(), "an annotation is not closed")
_ => ()
}
}
}
///|
fn Parser::parse_type(self : Parser) -> TType raise Refused {
let name = match self.peek() {
TIdent(name) => {
ignore(self.advance())
name
}
_ => refuse(self.file, self.line(), "expected a type")
}
let ty = match name {
"bool" => Bool
"byte" | "i8" => Byte
"i16" => I16
"i32" => I32
"i64" => I64
"double" => Double
"string" => Str
"binary" => Binary
"list" => {
self.expect('<')
let inner = self.parse_type()
self.expect('>')
List(inner)
}
"set" => {
self.expect('<')
let inner = self.parse_type()
self.expect('>')
Set(inner)
}
"map" => {
self.expect('<')
let key = self.parse_type()
self.expect(',')
let value = self.parse_type()
self.expect('>')
Map(key, value)
}
"void" => refuse(self.file, self.line(), "`void` is only a return type")
other => Named(other)
}
self.skip_annotation()
ty
}
///|
fn Parser::parse_struct(self : Parser, kind : DeclKind) -> Struct raise Refused {
let what = match kind {
PlainStruct => "a struct name"
ExceptionDecl => "an exception name"
UnionDecl => "a union name"
}
let name = self.expect_ident(what)
self.expect('{')
let fields : Array[Field] = []
while !self.eat('}') {
if self.peek() is TEof {
refuse(self.file, self.line(), "`\{name}` is not closed")
}
let id = self.expect_num()
self.expect(':')
let mut optional = false
if self.peek() is TIdent("optional") {
optional = true
ignore(self.advance())
} else if self.peek() is TIdent("required") {
ignore(self.advance())
}
let ty = self.parse_type()
let field_name = self.expect_ident("a field name")
self.skip_annotation()
if self.eat('=') {
refuse(
self.file,
self.line(),
"a field default is not supported; `\{field_name}` has one",
)
}
ignore(self.eat(',') || self.eat(';'))
fields.push({ id, name: field_name, ty, optional, })
}
self.skip_annotation()
{ name, fields, kind, }
}
///|
fn Parser::parse_enum(self : Parser) -> Enum raise Refused {
let name = self.expect_ident("an enum name")
self.expect('{')
let values : Array[(String, Int32)] = []
let mut next = 0
while !self.eat('}') {
if self.peek() is TEof {
refuse(self.file, self.line(), "`enum \{name}` is not closed")
}
let label = self.expect_ident("a value name")
let value = if self.eat('=') { self.expect_num() } else { next }
next = value + 1
ignore(self.eat(',') || self.eat(';'))
values.push((label, value))
}
self.skip_annotation()
{ name, values, }
}
///|
/// The `( id: Type name, ... )` a method's arguments and its `throws` clause both use.
fn Parser::parse_params(
self : Parser,
method_name : String,
) -> Array[Param] raise Refused {
self.expect('(')
let params : Array[Param] = []
while !self.eat(')') {
if self.peek() is TEof {
refuse(self.file, self.line(), "`\{method_name}` is not closed")
}
let id = self.expect_num()
self.expect(':')
let ty = self.parse_type()
let param_name = self.expect_ident("a parameter name")
ignore(self.eat(',') || self.eat(';'))
params.push({ id, name: param_name, ty, })
}
params
}
///|
fn Parser::parse_function(self : Parser) -> Function raise Refused {
if self.peek() is TIdent("oneway") {
// Say so here: read as a return type, `oneway void ping()` fails later with a
// "expected `(`" that points at the wrong thing.
refuse(
self.file,
self.line(),
"`oneway` is not supported; it would need a call that sends and reads no reply",
)
}
let ret = if self.peek() is TIdent("void") {
ignore(self.advance())
None
} else {
Some(self.parse_type())
}
let name = self.expect_ident("a method name")
let params = self.parse_params(name)
let exceptions = if self.peek() is TIdent("throws") {
ignore(self.advance())
self.parse_params(name)
} else {
[]
}
ignore(self.eat(',') || self.eat(';'))
{ name, ret, params, exceptions, }
}
///|
fn Parser::parse_service(self : Parser) -> Service raise Refused {
let name = self.expect_ident("a service name")
let mut base = None
if self.peek() is TIdent("extends") {
ignore(self.advance())
base = Some(self.expect_ident("the service it extends"))
}
self.expect('{')
let functions : Array[Function] = []
while !self.eat('}') {
if self.peek() is TEof {
refuse(self.file, self.line(), "`service \{name}` is not closed")
}
functions.push(self.parse_function())
}
self.skip_annotation()
{ name, base, functions, }
}
///|
/// One value: a literal, a list, a map or struct body, or `Enum.VALUE`. Arithmetic and
/// references to other constants are not read, and are refused where they appear.
fn Parser::parse_value(self : Parser) -> ConstValue raise Refused {
match self.peek() {
TNum(text) => {
ignore(self.advance())
let value = @string.parse_int64(text[:]) catch {
_ => refuse(self.file, self.line(), "`\{text}` is not a 64-bit number")
}
CInt(value)
}
TFloat(value) => {
ignore(self.advance())
CFloat(value)
}
TStr(text) => {
ignore(self.advance())
CStr(text)
}
TIdent(word) =>
match word {
"true" => {
ignore(self.advance())
CBool(true)
}
"false" => {
ignore(self.advance())
CBool(false)
}
_ => {
ignore(self.advance())
// `Mood.Happy` arrives as one word: a dot is part of an identifier here, which
// is also how `shared.Person` is written.
match split_dot(word) {
Some((enum_name, tag)) => CEnum(enum_name, tag)
None =>
refuse(
self.file,
self.line(),
"`\{word}` is not a value; a constant cannot name another constant",
)
}
}
}
TSym('[') => {
ignore(self.advance())
let items : Array[ConstValue] = []
while !self.eat(']') {
if self.peek() is TEof {
refuse(self.file, self.line(), "a list is not closed")
}
items.push(self.parse_value())
ignore(self.eat(',') || self.eat(';'))
}
CList(items)
}
TSym('{') => {
ignore(self.advance())
let entries : Array[(ConstValue, ConstValue)] = []
while !self.eat('}') {
if self.peek() is TEof {
refuse(self.file, self.line(), "a map or struct body is not closed")
}
let key = match self.peek() {
TStr(text) => {
ignore(self.advance())
CStr(text)
}
// A struct body may write its field names without quotes.
TIdent(word) => {
ignore(self.advance())
CStr(word)
}
_ => self.parse_value()
}
self.expect(':')
entries.push((key, self.parse_value()))
ignore(self.eat(',') || self.eat(';'))
}
CMap(entries)
}
_ => refuse(self.file, self.line(), "expected a value")
}
}
///|
fn Parser::parse_constant(
self : Parser,
line : Int32,
) -> Constant raise Refused {
let ty = self.parse_type()
let name = self.expect_ident("a constant name")
self.expect('=')
let value = self.parse_value()
self.skip_annotation()
ignore(self.eat(',') || self.eat(';'))
{ name, ty, value, line, }
}
///|
fn Parser::parse_document(self : Parser) -> Document raise Refused {
let structs : Array[Struct] = []
let enums : Array[Enum] = []
let services : Array[Service] = []
let typedefs : Array[Typedef] = []
let constants : Array[Constant] = []
let includes : Array[String] = []
while !self.at_end() {
match self.peek() {
TIdent("namespace") => {
ignore(self.advance())
ignore(self.expect_ident("a namespace scope"))
if self.peek() is TStr(_) {
ignore(self.advance())
} else if self.peek() is TIdent(_) {
ignore(self.advance())
}
ignore(self.eat(';'))
}
TIdent("include") => {
ignore(self.advance())
if self.peek() is TStr(path) {
ignore(self.advance())
includes.push(path)
} else {
refuse(self.file, self.line(), "`include` needs a quoted file")
}
ignore(self.eat(';'))
}
TIdent("cpp_include") => {
ignore(self.advance())
if self.peek() is TStr(_) {
ignore(self.advance())
}
ignore(self.eat(';'))
}
TIdent("struct") => {
ignore(self.advance())
structs.push(self.parse_struct(PlainStruct))
}
TIdent("exception") => {
ignore(self.advance())
structs.push(self.parse_struct(ExceptionDecl))
}
TIdent("enum") => {
ignore(self.advance())
enums.push(self.parse_enum())
}
TIdent("service") => {
ignore(self.advance())
services.push(self.parse_service())
}
TIdent("typedef") => {
ignore(self.advance())
let ty = self.parse_type()
let name = self.expect_ident("a typedef name")
self.skip_annotation()
ignore(self.eat(',') || self.eat(';'))
typedefs.push({ name, ty, })
}
TIdent("const") => {
let line = self.line()
ignore(self.advance())
constants.push(self.parse_constant(line))
}
TIdent("union") => {
ignore(self.advance())
structs.push(self.parse_struct(UnionDecl))
}
TIdent(other) =>
refuse(
self.file,
self.line(),
"`\{other}` is not something this compiler reads",
)
TSym(c) =>
refuse(
self.file,
self.line(),
"unexpected `\{c}` at the top level of the file",
)
_ =>
refuse(
self.file,
self.line(),
"unexpected end of an item at the top level",
)
}
}
{ structs, enums, services, typedefs, constants, includes, }
}
///|
/// Parse one file. The includes it names are collected, not read: folding them in is
/// `link`, which is where a `shared.Person` gets its name in the output.
pub fn parse(file : String, source : String) -> Document raise Refused {
let parser : Parser = { file, tokens: lex(file, source), pos: 0, }
parser.parse_document()
}