///|
/// Parse the EvoWitness line-oriented contract language.
///
/// Grammar (whitespace is insignificant):
///
/// ```text
/// contract
/// type
/// field [constraint=value ...]
/// end
/// ```
pub fn parse_contract(input : String) -> Result[Contract, ParseError] {
let normalized = input.replace_all(old="\r\n", new="\n")
let lines = normalized.split("\n").map(view => view.to_owned()).collect()
let mut contract_name : String? = None
let mut contract_version : String? = None
let objects : Array[ObjectType] = []
let mut current_name : String? = None
let mut current_open = false
let mut current_line = 0
let current_fields : Array[Field] = []
for index, raw_line in lines {
let line_number = index + 1
let source_line = raw_line
let code = strip_comment(raw_line).trim().to_owned()
if code.is_empty() {
continue
}
let tokens = split_words(code)
if tokens.is_empty() {
continue
}
match tokens[0] {
"contract" => {
if contract_name is Some(_) {
return Err(
parse_error(
line_number, "E002", "contract header may appear only once", source_line,
),
)
}
if current_name is Some(_) || !objects.is_empty() {
return Err(
parse_error(
line_number, "E003", "contract header must be the first declaration",
source_line,
),
)
}
if tokens.length() != 3 {
return Err(
parse_error(
line_number, "E004", "expected: contract ", source_line,
),
)
}
if !valid_identifier(tokens[1]) {
return Err(
parse_error(
line_number,
"E005",
"invalid contract name '" + tokens[1] + "'",
source_line,
),
)
}
if tokens[2].is_empty() {
return Err(
parse_error(
line_number, "E006", "contract version cannot be empty", source_line,
),
)
}
contract_name = Some(tokens[1])
contract_version = Some(tokens[2])
}
"type" => {
if contract_name is None {
return Err(
parse_error(
line_number, "E007", "declare the contract header before any type",
source_line,
),
)
}
if current_name is Some(_) {
return Err(
parse_error(
line_number, "E008", "nested type declarations are not allowed; add 'end' first",
source_line,
),
)
}
if tokens.length() != 3 ||
(tokens[2] != "open" && tokens[2] != "closed") {
return Err(
parse_error(
line_number, "E009", "expected: type ", source_line,
),
)
}
if !valid_identifier(tokens[1]) {
return Err(
parse_error(
line_number,
"E010",
"invalid type name '" + tokens[1] + "'",
source_line,
),
)
}
for object in objects {
if object.name == tokens[1] {
return Err(
parse_error(
line_number,
"E011",
"duplicate type '" + tokens[1] + "'",
source_line,
),
)
}
}
current_name = Some(tokens[1])
current_open = tokens[2] == "open"
current_line = line_number
current_fields.clear()
}
"field" => {
guard current_name is Some(_) else {
return Err(
parse_error(
line_number, "E012", "field declaration must be inside a type", source_line,
),
)
}
match parse_field(tokens, line_number, source_line) {
Err(error) => return Err(error)
Ok(field) => {
for existing in current_fields {
if existing.name == field.name {
return Err(
parse_error(
line_number,
"E013",
"duplicate field '" + field.name + "'",
source_line,
),
)
}
}
current_fields.push(field)
}
}
}
"end" => {
if tokens.length() != 1 {
return Err(
parse_error(
line_number, "E014", "'end' does not accept arguments", source_line,
),
)
}
guard current_name is Some(name) else {
return Err(
parse_error(
line_number, "E015", "unexpected 'end' outside a type", source_line,
),
)
}
objects.push({
name,
open: current_open,
fields: current_fields.copy(),
line: current_line,
})
current_name = None
current_fields.clear()
}
keyword =>
return Err(
parse_error(
line_number,
"E001",
"unknown declaration '" + keyword + "'",
source_line,
),
)
}
}
if current_name is Some(name) {
return Err(
parse_error(
current_line,
"E016",
"type '" + name + "' is missing its closing 'end'",
"type " + name,
),
)
}
guard contract_name is Some(name) && contract_version is Some(version) else {
return Err(parse_error(1, "E017", "missing contract header", ""))
}
if objects.is_empty() {
return Err(
parse_error(1, "E018", "contract must declare at least one type", ""),
)
}
let contract : Contract = { name, version, objects }
match validate_references(contract) {
Some(error) => Err(error)
None => Ok(contract)
}
}
///|
fn parse_field(
tokens : Array[String],
line_number : Int,
source_line : String,
) -> Result[Field, ParseError] {
if tokens.length() < 4 {
return Err(
parse_error(
line_number, "E020", "expected: field [constraint=value ...]",
source_line,
),
)
}
let name = tokens[1]
if !valid_identifier(name) {
return Err(
parse_error(
line_number,
"E021",
"invalid field name '" + name + "'",
source_line,
),
)
}
let type_expr = match parse_type_expr(tokens[2], line_number, source_line) {
Ok(value) => value
Err(error) => return Err(error)
}
let required = match tokens[3] {
"required" => true
"optional" => false
other =>
return Err(
parse_error(
line_number,
"E022",
"field presence must be 'required' or 'optional', found '" +
other +
"'",
source_line,
),
)
}
let mut min_int : Int? = None
let mut max_int : Int? = None
let mut min_len : Int? = None
let mut max_len : Int? = None
let mut default_value : String? = None
for index = 4; index < tokens.length(); index = index + 1 {
let token = tokens[index]
guard token.split_once("=") is Some((key_view, value_view)) else {
return Err(
parse_error(
line_number,
"E023",
"constraint must use key=value syntax, found '" + token + "'",
source_line,
),
)
}
let key = key_view.to_owned()
let value = value_view.to_owned()
if value.is_empty() {
return Err(
parse_error(
line_number,
"E024",
"constraint '" + key + "' cannot have an empty value",
source_line,
),
)
}
match key {
"min" =>
match parse_decimal(value) {
Some(number) => min_int = Some(number)
None =>
return Err(
parse_error(
line_number, "E025", "min must be a decimal integer", source_line,
),
)
}
"max" =>
match parse_decimal(value) {
Some(number) => max_int = Some(number)
None =>
return Err(
parse_error(
line_number, "E026", "max must be a decimal integer", source_line,
),
)
}
"minlen" =>
match parse_non_negative(value) {
Some(number) => min_len = Some(number)
None =>
return Err(
parse_error(
line_number, "E027", "minlen must be a non-negative integer", source_line,
),
)
}
"maxlen" =>
match parse_non_negative(value) {
Some(number) => max_len = Some(number)
None =>
return Err(
parse_error(
line_number, "E028", "maxlen must be a non-negative integer", source_line,
),
)
}
"default" => default_value = Some(value)
unknown =>
return Err(
parse_error(
line_number,
"E029",
"unknown constraint '" + unknown + "'",
source_line,
),
)
}
}
let constraints : Constraints = {
min_int,
max_int,
min_len,
max_len,
default_value,
}
match validate_constraints(type_expr, constraints, line_number, source_line) {
Some(error) => Err(error)
None => Ok({ name, type_expr, required, constraints, line: line_number })
}
}
///|
fn parse_type_expr(
token : String,
line_number : Int,
source_line : String,
) -> Result[TypeExpr, ParseError] {
match token {
"string" => Ok(StringType)
"int" => Ok(IntType)
"bool" => Ok(BoolType)
"number" => Ok(NumberType)
_ =>
if token.has_prefix("enum:") {
let values = token[5:]
.split("|")
.map(value => value.to_owned())
.collect()
if values.is_empty() || values.any(value => value.is_empty()) {
return Err(
parse_error(
line_number, "E030", "enum must contain one or more non-empty values",
source_line,
),
)
}
let seen : Array[String] = []
for value in values {
if !valid_enum_value(value) {
return Err(
parse_error(
line_number,
"E031",
"invalid enum value '" + value + "'",
source_line,
),
)
}
if seen.contains(value) {
return Err(
parse_error(
line_number,
"E032",
"duplicate enum value '" + value + "'",
source_line,
),
)
}
seen.push(value)
}
Ok(EnumType(values))
} else if token.has_prefix("ref:") {
let name = token[4:].to_owned()
if !valid_identifier(name) {
Err(
parse_error(
line_number,
"E033",
"invalid referenced type '" + name + "'",
source_line,
),
)
} else {
Ok(RefType(name))
}
} else if token.has_prefix("list:") {
let item = token[5:].to_owned()
if valid_list_item(item) {
Ok(ListType(item))
} else {
Err(
parse_error(
line_number, "E034", "list item must be string, int, bool, number, or ref:",
source_line,
),
)
}
} else {
Err(
parse_error(
line_number,
"E035",
"unsupported field type '" + token + "'",
source_line,
),
)
}
}
}
///|
fn validate_constraints(
type_expr : TypeExpr,
constraints : Constraints,
line_number : Int,
source_line : String,
) -> ParseError? {
if constraints.min_int is Some(_) || constraints.max_int is Some(_) {
if type_expr != IntType && type_expr != NumberType {
return Some(
parse_error(
line_number, "E040", "min/max constraints are valid only for int and number fields",
source_line,
),
)
}
}
if constraints.min_len is Some(_) || constraints.max_len is Some(_) {
if type_expr != StringType && !(type_expr is ListType(_)) {
return Some(
parse_error(
line_number, "E041", "minlen/maxlen constraints are valid only for string and list fields",
source_line,
),
)
}
}
if constraints.min_int is Some(minimum) &&
constraints.max_int is Some(maximum) {
if minimum > maximum {
return Some(
parse_error(
line_number, "E042", "min cannot be greater than max", source_line,
),
)
}
}
if constraints.min_len is Some(minimum) &&
constraints.max_len is Some(maximum) {
if minimum > maximum {
return Some(
parse_error(
line_number, "E043", "minlen cannot be greater than maxlen", source_line,
),
)
}
}
None
}
///|
fn validate_references(contract : Contract) -> ParseError? {
for object in contract.objects {
for field in object.fields {
match field.type_expr.referenced_name() {
Some(name) =>
if contract.find_object(name) is None {
return Some(
parse_error(
field.line,
"E050",
"field '" +
field.name +
"' references missing type '" +
name +
"'",
"field " + field.name + " " + field.type_expr.render(),
),
)
}
None => ()
}
}
}
None
}
///|
fn strip_comment(line : String) -> String {
match line.find("#") {
Some(offset) => line[:offset].to_owned()
None => line
}
}
///|
fn split_words(line : String) -> Array[String] {
let words : Array[String] = []
for piece in line.split(" ") {
let word = piece.trim()
if !word.is_empty() {
words.push(word.to_owned())
}
}
words
}
///|
fn valid_identifier(value : String) -> Bool {
if value.is_empty() {
return false
}
let chars = value.to_array()
if !(chars[0].is_ascii_alphabetic() || chars[0] == '_') {
return false
}
for char in chars {
if !(char.is_ascii_alphabetic() ||
char.is_ascii_digit() ||
char == '_' ||
char == '-') {
return false
}
}
true
}
///|
fn valid_enum_value(value : String) -> Bool {
if value.is_empty() {
return false
}
value.all(char => {
char.is_ascii_alphabetic() ||
char.is_ascii_digit() ||
char == '_' ||
char == '-' ||
char == '.'
})
}
///|
fn valid_list_item(value : String) -> Bool {
value == "string" ||
value == "int" ||
value == "bool" ||
value == "number" ||
(value.has_prefix("ref:") && valid_identifier(value[4:].to_owned()))
}
///|
fn parse_non_negative(value : String) -> Int? {
match parse_decimal(value) {
Some(number) if number >= 0 => Some(number)
_ => None
}
}
///|
fn parse_decimal(value : String) -> Int? {
if value.is_empty() {
return None
}
let chars = value.to_array()
let mut index = 0
let mut sign = 1
if chars[0] == '-' {
if chars.length() == 1 {
return None
}
sign = -1
index = 1
} else if chars[0] == '+' {
if chars.length() == 1 {
return None
}
index = 1
}
let mut result = 0
while index < chars.length() {
let char = chars[index]
if !char.is_ascii_digit() {
return None
}
let digit = char.to_int() - '0'.to_int()
result = result * 10 + digit
index = index + 1
}
Some(result * sign)
}