// Source round-trip: every expression prints back as its original syntax.
//
// ONE printer, for the reason there is one AST. It used to be two — this one
// over the slot vocabulary, and `tscript/script_print.mbt`'s over the block
// grammar — and they disagreed about small things (whether a newline in a
// literal is escaped, whether a zero-argument application keeps its trailing
// space) that nobody chose and nobody could see.
//
// Canonical, not faithful. Parentheses are emitted wherever the grammar
// REQUIRES them rather than wherever the author wrote them, so printing is a
// normal form: two spellings of the same expression print identically, which
// is exactly what makes a round-trip comparison meaningful.
///|
fn op_word(op : UnOp) -> String {
match op {
UNot => "not "
UNeg => "-"
}
}
///|
fn lit_source(l : Lit) -> String {
match l {
LNull => "null"
LBool(b) => if b { "true" } else { "false" }
LNum(n) => num_source(n)
LStr(s) => escape_str_literal(s)
}
}
///|
/// A place as source: the root's sigil, then every step attached.
pub fn Place::to_source(self : Place) -> String {
let b = StringBuilder()
match self.root {
PState(n) => {
b.write_char('.')
b.write_string(n)
}
PBind(n) => {
b.write_char('@')
b.write_string(n)
}
// No sigil either: `cur` is not a binding, and writing it as one was the
// whole of what `@cur` got wrong.
PTarget => b.write_string(target_bind)
// No sigil: a parameter is written the way it was declared, and the steps
// below it print attached, which is the only spelling that reads back.
PParam(n) => b.write_string(n)
}
for st in self.steps {
match st {
PField(n) => {
b.write_char('.')
b.write_string(n)
}
PIndex(e) => {
b.write_char('[')
b.write_string(val_source(e))
b.write_char(']')
}
}
}
b.to_string()
}
///|
/// True when an expression needs parentheses to sit where an OPERAND is
/// expected — as an argument of an application, or beside an operator.
///
/// An atom never does. Everything with an operator or a juxtaposition in it
/// always does, and that is the grammar's own rule rather than a precedence
/// judgement: the language has no precedence, so anything compound in an
/// operand position is written parenthesized or is not written at all.
fn needs_parens(e : Expr) -> Bool {
match e {
EApp(args~, ..) => !args.is_empty()
EChain(..) | EUnary(..) | EIf(..) => true
_ => false
}
}
///|
pub fn operand_source(e : Expr) -> String {
if needs_parens(e) {
"(" + val_source(e) + ")"
} else {
val_source(e)
}
}
///|
fn val_source(val : Expr) -> String {
match val {
// `e` is the root and never a segment, so it is written back rather than
// stored — there is no expression that means "the event".
EEventPath(segments~, ..) => "e." + segments.join(".")
ELit(lit~, ..) => lit_source(lit)
ETpl(parts~, ..) => {
let b = StringBuilder()
b.write_string("$'")
for part in parts {
match part {
// Re-escape the raw text (the parse unescaped it once).
TText(text~, ..) => escape_str_into(b, text)
TExpr(inner) => {
b.write_char('{')
b.write_string(val_source(inner))
b.write_char('}')
}
}
}
b.write_char('\'')
b.to_string()
}
EApp(name~, args~, ..) =>
// A zero-argument application prints as the bare name, so the round trip
// holds once the vocabulary admits one.
if args.is_empty() {
name
} else if is_infix_op(name) && args.length() == 2 {
// A comparison is written INFIX and has to print that way. The view
// slot parser builds one as an application — the same node a named
// builtin makes — so the shape says nothing about the spelling, and a
// printer that ignored the difference would answer a form nothing
// parses.
operand_source(args[0]) + " " + name + " " + operand_source(args[1])
} else {
name + " " + args.map(a => operand_source(a)).join(" ")
}
EName(name~, ..) => name
ETypeName(name~, ..) => name
EDyn(name~, ..) => "*" + name
EMethod(name~, ..) => "$" + name
EMacroVar(name~, ..) => "^" + name
EConfigVar(name~, ..) => "host." + name
ERead(place~, ..) => place.to_source()
ERef(place~, ..) => "&" + place.to_source()
EUnary(op~, operand~, ..) => op_word(op) + operand_source(operand)
EChain(ops~, operands~, ..) => {
let b = StringBuilder()
b.write_string(operand_source(operands[0]))
for i, op in ops {
b.write_char(' ')
b.write_string(op)
b.write_char(' ')
b.write_string(operand_source(operands[i + 1]))
}
b.to_string()
}
EIf(cond~, then_~, else_~, ..) =>
"if " +
val_source(cond) +
" { " +
val_source(then_) +
" } else { " +
val_source(else_) +
" }"
}
}
///|
/// Print an expression back as canonical source. Round-tripping through this
/// is how the grammar is pinned: parse, print, parse again, and the two ASTs
/// agree.
pub fn Expr::to_source(self : Expr) -> String {
val_source(self)
}
///|
pub impl Show for Expr with fn output(self, logger) {
logger.write_string(val_source(self))
}
///|
/// Written between its operands rather than in front of them. The compare
/// family and nothing else: `and` / `or` / `implies` are named applications in
/// a view slot and read as such.
fn is_infix_op(name : String) -> Bool {
name is ("is" | "is not" | "<" | "<=" | ">" | ">=")
}