///|
pub(all) struct IdlLocation {
source : String
offset : Int
line : Int
column : Int
} derive(Debug, Eq, ToJson)
///|
pub suberror SchemaError {
Syntax(String, IdlLocation)
InvalidSchema(String)
} derive(Debug)
///|
pub(all) enum IdlType {
Base(String)
Named(String)
ListOf(IdlType)
SetOf(IdlType)
MapOf(IdlType, IdlType)
} derive(Debug, Eq)
///|
pub(all) enum IdlConst {
IntegerConstant(Int64)
FloatConstant(Double)
StringConstant(String)
NameConstant(String)
ListConstant(Array[IdlConst])
MapConstant(Array[(IdlConst, IdlConst)])
} derive(Debug, Eq)
///|
pub(all) struct IdlField {
id : Int
name : String
field_type : IdlType
requiredness : String
default_value : IdlConst?
annotations : Map[String, String]
location : IdlLocation
} derive(Debug, Eq)
///|
pub(all) struct IdlMethod {
name : String
return_type : IdlType
oneway : Bool
arguments : Array[IdlField]
exceptions : Array[IdlField]
annotations : Map[String, String]
location : IdlLocation
} derive(Debug, Eq)
///|
pub(all) enum IdlDefinition {
Alias(String, IdlType, Map[String, String])
Enumeration(String, Array[(String, Int)], Map[String, String])
Record(String, String, Array[IdlField], Map[String, String])
Constant(String, IdlType, IdlConst)
Service(String, String?, Array[IdlMethod], Map[String, String])
} derive(Debug, Eq)
///|
pub(all) struct IdlModule {
source : String
includes : Array[String]
namespaces : Map[String, String]
cpp_includes : Array[String]
definitions : Array[IdlDefinition]
} derive(Debug, Eq)
///|
priv struct IdlToken {
text : String
quoted : Bool
location : IdlLocation
}
///|
priv struct IdlScanner {
chars : Array[Char]
mut pos : Int
mut line : Int
mut column : Int
source : String
}
///|
fn IdlScanner::location(self : IdlScanner) -> IdlLocation {
{
source: self.source,
offset: self.pos,
line: self.line,
column: self.column,
}
}
///|
fn IdlScanner::peek(self : IdlScanner, n? : Int = 0) -> Char? {
self.chars.get(self.pos + n)
}
///|
fn IdlScanner::take(self : IdlScanner) -> Char {
let c = self.chars[self.pos]
self.pos += 1
if c == '\n' {
self.line += 1
self.column = 1
} else {
self.column += 1
}
c
}
///|
fn idl_alpha(c : Char) -> Bool {
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'
}
///|
fn idl_digit(c : Char) -> Bool {
c >= '0' && c <= '9'
}
///|
fn idl_reserved(name : String) -> Bool {
[
"include", "cpp_include", "namespace", "typedef", "const", "enum", "struct",
"union", "exception", "service", "extends", "oneway", "required", "optional",
"throws", "cpp_type", "xsd_all", "xsd_optional", "xsd_nillable", "xsd_attrs",
].contains(name)
}
///|
fn idl_lex(text : String, source : String) -> Array[IdlToken] raise SchemaError {
if text.length() > 1000000 {
raise InvalidSchema("IDL source exceeds 1000000 UTF-16 units")
}
let scan : IdlScanner = {
chars: text.to_array(),
pos: 0,
line: 1,
column: 1,
source,
}
let tokens = []
while scan.pos < scan.chars.length() {
let c = scan.chars[scan.pos]
if [' ', '\t', '\r', '\n', '\u000c'].contains(c) ||
(scan.pos == 0 && c == '\uFEFF') {
ignore(scan.take())
continue
}
if c == '#' || (c == '/' && scan.peek(n=1) == Some('/')) {
while scan.peek() != None && scan.peek() != Some('\n') {
ignore(scan.take())
}
continue
}
if c == '/' && scan.peek(n=1) == Some('*') {
let start = scan.location()
ignore(scan.take())
ignore(scan.take())
let mut closed = false
while scan.peek() != None {
if scan.peek() == Some('*') && scan.peek(n=1) == Some('/') {
ignore(scan.take())
ignore(scan.take())
closed = true
break
}
ignore(scan.take())
}
if !closed {
raise Syntax("unterminated comment", start)
}
continue
}
let location = scan.location()
let start = scan.pos
if c == '\'' || c == '"' {
ignore(scan.take())
let out = StringBuilder()
let mut closed = false
while scan.peek() != None {
let ch = scan.take()
if ch == c {
closed = true
break
}
if ch == '\n' || ch == '\r' {
raise Syntax("newline in string literal", location)
}
if ch == '\\' {
if scan.peek() == None {
raise Syntax("incomplete string escape", location)
}
let escape = scan.take()
match escape {
'n' => out.write_char('\n')
'r' => out.write_char('\r')
't' => out.write_char('\t')
'b' => out.write_char('\u0008')
'f' => out.write_char('\u000c')
'\\' => out.write_char('\\')
'\'' => out.write_char('\'')
'"' => out.write_char('"')
_ => raise Syntax("unsupported string escape", location)
}
} else {
out.write_char(ch)
}
}
if !closed {
raise Syntax("unterminated string literal", location)
}
tokens.push({ text: out.to_string(), quoted: true, location, })
} else if idl_alpha(c) {
ignore(scan.take())
while scan.peek() is Some(ch) &&
(idl_alpha(ch) || idl_digit(ch) || ch == '.') {
ignore(scan.take())
}
tokens.push({
text: String::from_array(scan.chars[start:scan.pos]),
quoted: false,
location,
})
} else if idl_digit(c) ||
c == '+' ||
c == '-' ||
(c == '.' && scan.peek(n=1) is Some(n) && idl_digit(n)) {
ignore(scan.take())
while scan.peek() is Some(ch) &&
(
idl_alpha(ch) ||
idl_digit(ch) ||
ch == '.' ||
(
(ch == '+' || ch == '-') &&
(
scan.chars[scan.pos - 1] == 'e' ||
scan.chars[scan.pos - 1] == 'E'
)
)
) {
ignore(scan.take())
}
tokens.push({
text: String::from_array(scan.chars[start:scan.pos]),
quoted: false,
location,
})
} else if ['{', '}', '[', ']', '(', ')', '<', '>', ':', ',', ';', '=', '*'].contains(
c,
) {
ignore(scan.take())
tokens.push({ text: c.to_string(), quoted: false, location, })
} else {
raise Syntax("unexpected character " + c.to_string(), location)
}
if tokens.length() > 200000 {
raise InvalidSchema("IDL token limit")
}
}
tokens.push({ text: "", quoted: false, location: scan.location(), })
tokens
}
///|
priv struct IdlParser {
tokens : Array[IdlToken]
mut pos : Int
mut work : Int
}
///|
fn IdlParser::peek(self : IdlParser) -> String {
if self.tokens[self.pos].quoted {
""
} else {
self.tokens[self.pos].text
}
}
///|
fn IdlParser::take(self : IdlParser) -> IdlToken raise SchemaError {
let token = self.tokens[self.pos]
if self.peek() == "" {
raise Syntax("unexpected end", token.location)
}
self.pos += 1
self.work += 1
if self.work > 200000 {
raise InvalidSchema("IDL parse work limit")
}
token
}
///|
fn IdlParser::eat(self : IdlParser, text : String) -> Bool {
if self.peek() == text {
self.pos += 1
true
} else {
false
}
}
///|
fn IdlParser::need(self : IdlParser, text : String) -> Unit raise SchemaError {
if !self.eat(text) {
raise Syntax(
"expected " + text + ", got " + self.peek(),
self.tokens[self.pos].location,
)
}
}
///|
fn IdlParser::name(self : IdlParser) -> String raise SchemaError {
let token = self.take()
let cs = token.text.to_array()
if token.quoted ||
cs.is_empty() ||
!idl_alpha(cs[0]) ||
idl_reserved(token.text) {
raise Syntax("expected non-reserved identifier", token.location)
}
token.text
}
///|
fn IdlParser::literal(self : IdlParser) -> String raise SchemaError {
let token = self.take()
if !token.quoted {
raise Syntax("expected string literal", token.location)
}
token.text
}
///|
fn IdlParser::separator(self : IdlParser) -> Unit {
if self.peek() == "," || self.peek() == ";" {
self.pos += 1
}
}
///|
fn IdlParser::annotations(
self : IdlParser,
) -> Map[String, String] raise SchemaError {
let annotations = Map([])
if self.eat("(") {
while !self.eat(")") {
let name = self.name()
let value = if self.eat("=") { self.literal() } else { "1" }
annotations[name] = value
self.separator()
}
}
annotations
}
///|
fn IdlParser::field_type(
self : IdlParser,
depth : Int,
) -> IdlType raise SchemaError {
if depth > 64 {
raise InvalidSchema("IDL type nesting limit")
}
let token = self.take()
let name = token.text
if token.quoted {
raise Syntax("expected type", token.location)
}
if [
"bool", "byte", "i8", "i16", "i32", "i64", "double", "string", "binary", "uuid",
"void",
].contains(name) {
return Base(if name == "i8" { "byte" } else { name })
}
if ["list", "set", "map"].contains(name) {
if self.eat("cpp_type") {
ignore(self.literal())
}
self.need("<")
let first = self.field_type(depth + 1)
let result = if name == "map" {
self.need(",")
MapOf(first, self.field_type(depth + 1))
} else if name == "set" {
SetOf(first)
} else {
ListOf(first)
}
self.need(">")
if self.eat("cpp_type") {
ignore(self.literal())
}
ignore(self.annotations())
return result
}
if name.to_array().get(0) is Some(c) && idl_alpha(c) && !idl_reserved(name) {
Named(name)
} else {
raise Syntax("invalid type", token.location)
}
}
///|
fn idl_integer(token : IdlToken) -> Int64 raise SchemaError {
let text = token.text
let cs = text.to_array()
let mut start = 0
let mut sign = ""
if cs.get(0) == Some('+') || cs.get(0) == Some('-') {
if cs[0] == '-' {
sign = "-"
}
start = 1
}
let hex = cs.get(start) == Some('0') &&
(cs.get(start + 1) == Some('x') || cs.get(start + 1) == Some('X'))
if hex {
start += 2
}
if start >= cs.length() {
raise Syntax("expected integer", token.location)
}
for i in start.. raise Syntax("integer out of signed 64-bit range", token.location)
}
}
///|
fn idl_double(token : IdlToken) -> Double raise SchemaError {
let cs = token.text.to_array()
let mut i = 0
let mut digits = 0
if cs.get(i) == Some('+') || cs.get(i) == Some('-') {
i += 1
}
while i < cs.length() && idl_digit(cs[i]) {
i += 1
digits += 1
}
if cs.get(i) == Some('.') {
i += 1
while i < cs.length() && idl_digit(cs[i]) {
i += 1
digits += 1
}
}
if digits == 0 {
raise Syntax("invalid floating constant", token.location)
}
if cs.get(i) == Some('e') || cs.get(i) == Some('E') {
i += 1
if cs.get(i) == Some('+') || cs.get(i) == Some('-') {
i += 1
}
let start = i
while i < cs.length() && idl_digit(cs[i]) {
i += 1
}
if i == start {
raise Syntax("invalid floating exponent", token.location)
}
}
if i != cs.length() {
raise Syntax("invalid floating constant", token.location)
}
@strconv.parse_double(token.text) catch {
_ => if token.text.has_prefix("-") { -1.0 / 0.0 } else { 1.0 / 0.0 }
}
}
///|
fn IdlParser::constant(
self : IdlParser,
depth : Int,
) -> IdlConst raise SchemaError {
if depth > 64 {
raise InvalidSchema("IDL constant nesting limit")
}
if self.eat("[") {
let values = []
while !self.eat("]") {
values.push(self.constant(depth + 1))
self.separator()
}
return ListConstant(values)
}
if self.eat("{") {
let values = []
while !self.eat("}") {
let key = self.constant(depth + 1)
self.need(":")
values.push((key, self.constant(depth + 1)))
self.separator()
}
return MapConstant(values)
}
let token = self.take()
if token.quoted {
return StringConstant(token.text)
}
let cs = token.text.to_array()
if !cs.is_empty() &&
(idl_digit(cs[0]) || cs[0] == '+' || cs[0] == '-' || cs[0] == '.') {
let hex = token.text.to_lower().contains("0x")
if !hex && (token.text.contains(".") || token.text.to_lower().contains("e")) {
return FloatConstant(idl_double(token))
}
return IntegerConstant(idl_integer(token))
}
if !cs.is_empty() && idl_alpha(cs[0]) {
NameConstant(token.text)
} else {
raise Syntax("expected constant", token.location)
}
}
///|
fn IdlParser::fields(
self : IdlParser,
end : String,
union : Bool,
depth? : Int = 0,
) -> Array[IdlField] raise SchemaError {
if depth > 64 {
raise InvalidSchema("IDL field metadata nesting limit")
}
let fields = []
let ids : Map[Int, Bool] = Map([])
let names : Map[String, Bool] = Map([])
let mut automatic = -1
while !self.eat(end) {
let location = self.tokens[self.pos].location
let explicit = self.tokens.get(self.pos + 1) is Some(token) &&
token.text == ":"
let id = if explicit {
let n = idl_integer(self.take())
self.need(":")
if n > 2147483647L {
raise Syntax("field id exceeds metadata integer range", location)
}
if n > 0L {
n.to_int()
} else {
let n = automatic
automatic -= 1
n
}
} else {
let n = automatic
automatic -= 1
n
}
if automatic < -32769 {
raise Syntax("automatic field id limit", location)
}
let req = if self.eat("required") {
"required"
} else if self.eat("optional") {
"optional"
} else {
"req_out"
}
let field_type = self.field_type(0)
let name = self.name()
if field_type == Base("void") {
raise Syntax("field cannot have void type", location)
}
let default_value = if self.eat("=") {
Some(self.constant(0))
} else {
None
}
while self.eat("xsd_optional") || self.eat("xsd_nillable") {
}
if self.eat("xsd_attrs") {
self.need("{")
ignore(self.fields("}", false, depth=depth + 1))
}
let annotations = self.annotations()
self.separator()
if ids.contains(id) || names.contains(name) {
raise Syntax("duplicate field id or name", location)
}
ids[id] = true
names[name] = true
fields.push({
id,
name,
field_type,
requiredness: if union {
"optional"
} else {
req
},
default_value,
annotations,
location,
})
if fields.length() > 10000 {
raise InvalidSchema("IDL field count limit")
}
}
fields
}
///|
/// Parse one Thrift IDL document. Includes and named references are resolved by compile_schema.
pub fn parse_idl(
text : String,
source? : String = "main.thrift",
) -> IdlModule raise SchemaError {
let parser : IdlParser = { tokens: idl_lex(text, source), pos: 0, work: 0, }
let includes = []
let namespaces = Map([])
let cpp_includes = []
let definitions = []
let names : Map[String, Bool] = Map([])
while parser.peek() != "" {
let token = parser.take()
let keyword = token.text
if !definitions.is_empty() &&
["include", "cpp_include", "namespace"].contains(keyword) {
raise Syntax("IDL headers must precede definitions", token.location)
}
if keyword == "include" {
includes.push(parser.literal())
parser.separator()
continue
}
if keyword == "cpp_include" {
cpp_includes.push(parser.literal())
parser.separator()
continue
}
if keyword == "namespace" {
let scope = if parser.eat("*") { "*" } else { parser.name() }
namespaces[scope] = if parser.peek() == "" {
parser.literal()
} else {
parser.name()
}
ignore(parser.annotations())
parser.separator()
continue
}
let definition : IdlDefinition = match keyword {
"typedef" => {
let t = parser.field_type(0)
let name = parser.name()
Alias(name, t, parser.annotations())
}
"const" => {
let t = parser.field_type(0)
let name = parser.name()
parser.need("=")
Constant(name, t, parser.constant(0))
}
"enum" => {
let name = parser.name()
parser.need("{")
let members = []
let used : Map[String, Bool] = Map([])
let mut next = 0L
while !parser.eat("}") {
let location = parser.tokens[parser.pos].location
let item_name = parser.name()
let value = if parser.eat("=") {
idl_integer(parser.take())
} else {
next
}
if value < -2147483648L || value > 2147483647L {
raise Syntax("enum value out of signed 32-bit range", location)
}
if used.contains(item_name) {
raise Syntax("duplicate enum member", location)
}
used[item_name] = true
members.push((item_name, value.to_int()))
next = value + 1L
ignore(parser.annotations())
parser.separator()
}
Enumeration(name, members, parser.annotations())
}
"struct" | "union" | "exception" => {
let name = parser.name()
ignore(parser.eat("xsd_all"))
parser.need("{")
Record(
name,
keyword,
parser.fields("}", keyword == "union"),
parser.annotations(),
)
}
"service" => {
let name = parser.name()
let parent = if parser.eat("extends") {
Some(parser.name())
} else {
None
}
parser.need("{")
let methods = []
let used : Map[String, Bool] = Map([])
while !parser.eat("}") {
let location = parser.tokens[parser.pos].location
let oneway = parser.eat("oneway")
let return_type = parser.field_type(0)
let function_name = parser.name()
parser.need("(")
let arguments = parser.fields(")", false)
let exceptions = if parser.eat("throws") {
parser.need("(")
parser.fields(")", false)
} else {
[]
}
if used.contains(function_name) {
raise Syntax("duplicate service method", location)
}
used[function_name] = true
if oneway && !exceptions.is_empty() {
raise Syntax("oneway method cannot throw", location)
}
let annotations = parser.annotations()
parser.separator()
methods.push({
name: function_name,
return_type,
oneway,
arguments,
exceptions,
annotations,
location,
})
}
Service(name, parent, methods, parser.annotations())
}
_ =>
raise Syntax("expected IDL definition, got " + keyword, token.location)
}
let name = match definition {
Alias(n, _, _)
| Constant(n, _, _)
| Enumeration(n, _, _)
| Record(n, _, _, _)
| Service(n, _, _, _) => n
}
if names.contains(name) {
raise Syntax("duplicate definition " + name, token.location)
}
names[name] = true
definitions.push(definition)
parser.separator()
if definitions.length() > 10000 {
raise InvalidSchema("IDL definition count limit")
}
}
{ source, includes, namespaces, cpp_includes, definitions, }
}