///|
/// Expression AST as JSON.
///
/// The ast operation converts between expression source and an explicit
/// JSON tree: `{"kind": "binary", "op": "+", "left": ..., "right": ...}`
/// with literal leaves carrying their value and call nodes carrying
/// their argument list. The `parse` direction accepts such a tree and
/// compiles it back to an expression, rejecting unknown kinds and
/// malformed nodes with coded errors. This is the stable interchange
/// format for external tooling: nothing else in the API exposes tree
/// structure as data.
///|
fn Expression::to_ast(self : Expression) -> Value {
match self {
Literal(value) => record([("kind", String("literal")), ("value", value)])
Variable(path) =>
record([("kind", String("variable")), ("path", String(path))])
Unary(op, operand) =>
record([
("kind", String("unary")),
("op", String(op)),
("operand", operand.to_ast()),
])
Binary(op, left, right) =>
record([
("kind", String("binary")),
("op", String(op)),
("left", left.to_ast()),
("right", right.to_ast()),
])
Call(name, args) =>
record([
("kind", String("call")),
("name", String(name)),
("args", Array(args.map(fn(arg) { arg.to_ast() }))),
])
}
}
///|
fn Expression::from_ast(value : Value) -> Expression raise {
let kind = str(get(value, "kind"))
match kind {
"literal" => {
let literal = get(value, "value")
if literal is Null {
raise input_error("E3001", "literal node needs a value")
}
Literal(literal)
}
"variable" => Variable(str(get(value, "path")))
"unary" => {
let op = str(get(value, "op"))
if op != "!" && op != "-" {
raise input_error("E2004", "unsupported unary operator: " + op)
}
Unary(op, Expression::from_ast(get(value, "operand")))
}
"binary" => {
let op = str(get(value, "op"))
if precedence(op) == 0 {
raise input_error("E2005", "unknown operator: " + op)
}
Binary(
op,
Expression::from_ast(get(value, "left")),
Expression::from_ast(get(value, "right")),
)
}
"call" => {
let name = str(get(value, "name"))
let allowed = [
"exists", "len", "contains", "lower", "upper", "trim", "starts_with", "ends_with",
"replace", "substring", "abs", "floor", "ceil", "round", "min", "max", "sum",
"concat", "count_matches", "any_match", "all_match",
]
if !allowed.contains(name) {
raise input_error("E1008", "unknown function: " + name)
}
let args : Array[Expression] = []
for item in arr(get(value, "args")) {
args.push(Expression::from_ast(item))
}
Call(name, args)
}
other => raise input_error("E3009", "unknown node kind: " + other)
}
}
///|
fn ast_request(request : Value) -> Value raise {
match get(request, "expression") {
Null => ()
source => {
let expr = compile(str(source))
return record([
("ast", expr.to_ast()),
("canonical", String(expr.render())),
])
}
}
let tree = get(request, "ast")
if tree is Null {
raise input_error("E3001", "ast operation needs expression or ast")
}
let expr = Expression::from_ast(tree)
let rendered = expr.render()
ignore(compile(rendered))
let (nodes, depth) = expr.metrics()
record([
("canonical", String(rendered)),
("nodes", Number(nodes.to_double())),
("depth", Number(depth.to_double())),
])
}