///|
/// The commands that drive the differential suite.
///
/// Their output formats are shared with `tools/oracle/`, not chosen for looks:
/// a file's records are compared line for line against what the reference
/// produced. Changing one of these changes what the suite compares, so change
/// the oracle in the same commit or the goldens stop meaning anything.
///
/// Every one takes many files in one process. There are 696 of them and both
/// sides pay process startup, which is the difference between a gate people run
/// and one they stop running.
fn cmd_tokens(files : Array[String]) -> Int32 {
each_file(files, (path, src) => {
let buf = StringBuilder()
for t in @lexer.lex_all(src) {
if t.kind is EndOfInput {
continue
}
buf.write_string(t.kind.kind_name())
buf.write_char('\t')
buf.write_string(t.line().to_string())
buf.write_char('\t')
buf.write_string(t.column().to_display())
buf.write_char('\t')
buf.write_string(escape_text(t.raw_text()))
buf.write_char('\t')
match t.kind {
Literal(d) => buf.write_string(d.canonical())
_ => buf.write_char('-')
}
buf.write_char('\n')
}
ignore(path)
print_trimmed(buf.to_string())
})
}
///|
/// The parse tree, or the failure.
///
/// A rejected file prints `!error ` rather than exiting, so that ONE
/// comparison covers both parse parity and error parity: a port that accepted a
/// file the reference rejects, or rejected one it accepts, differs here rather
/// than in a second oracle that could disagree with this one.
fn cmd_parse(files : Array[String]) -> Int32 {
each_file(files, (_, src) => {
let line = @parser.parse(src).root().canonical() catch {
ShrubberyError(d) => "!error " + d.kind.reference_text()
}
println(line)
})
}
///|
/// The source rebuilt from the tree.
///
/// Between `<<<<` and `>>>>`, with a NUL before the closing marker, so that
/// trailing whitespace and a missing final newline survive the harness reading
/// it back.
fn cmd_source(files : Array[String]) -> Int32 {
each_file(files, (_, src) => {
let rebuilt = @parser.parse(src).root().to_source() catch {
_ => {
println("!error")
return
}
}
println("<<<<")
println(rebuilt + "\u{0}")
println(">>>>")
})
}
///|
/// The eleven layout modes, in the order the reference's own suite runs them.
let print_modes : Array[(String, @write.Style, Int32?)] = [
("flat", Flat, None),
("pretty", Pretty, None),
("pretty-0", Pretty, Some(0)),
("pretty-40", Pretty, Some(40)),
("armor", Armoured, None),
("armor-0", Armoured, Some(0)),
("armor-40", Armoured, Some(40)),
("multi", PreferMultiline, None),
("multi-0", PreferMultiline, Some(0)),
("multi-40", PreferMultiline, Some(40)),
("pretty-120", Pretty, Some(120)),
]
///|
/// One record per mode, newlines escaped, so that a record is a line and a
/// divergence names the mode it happened in.
fn cmd_print(files : Array[String]) -> Int32 {
each_file(files, (_, src) => {
let root = @parser.parse(src).root() catch {
_ => {
println("!error")
return
}
}
let buf = StringBuilder()
for m in print_modes {
let (name, style, width) = m
buf.write_string(name)
buf.write_char('\t')
escape_lines(buf, @write.write(root, style~, width~))
buf.write_char('\n')
}
print_trimmed(buf.to_string())
})
}
///|
/// The layout document rather than a layout of it.
///
/// The debugging tool for the printer: it separates a LAYOUT disagreement --
/// the renderer chose differently -- from a CONSTRUCTION one, where the two
/// documents were never the same to begin with.
fn cmd_doc(args : Array[String]) -> Int32 {
if args.length() < 2 {
println("usage: shrubbery doc FILE...")
return 2
}
let style : @write.Style = match args[0] {
"armor" => Armoured
"multi" => PreferMultiline
_ => Pretty
}
each_file(args[1:].to_owned(), (_, src) => {
let root = @parser.parse(src).root() catch {
_ => {
println("!error")
return
}
}
println(@write.to_doc(root, style~).to_sexpr())
})
}
///|
/// Run `body` over each file, with the `==== path` header the harness splits
/// on. Unreadable files get a marker rather than stopping the batch.
fn each_file(files : Array[String], body : (String, String) -> Unit) -> Int32 {
for path in files {
println("==== \{path}")
match read_quiet(path) {
Some(src) => body(path, src)
None => println("!!!! cannot read")
}
}
0
}
///|
fn read_quiet(path : String) -> String? {
Some(@fs.read_file_to_string(path)) catch {
_ => None
}
}
///|
/// The escaping `@sexp.Datum::canonical` uses, so both sides of a comparison
/// are all-ASCII and an encoding difference in transit cannot look like a
/// lexing difference.
fn escape_text(s : String) -> String {
let buf = StringBuilder()
buf.write_char('"')
for c in s {
let u = c.to_int()
if c == '\\' || c == '"' || c == '|' {
buf.write_char('\\')
buf.write_char(c)
} else if u >= 0x20 && u < 0x7F {
buf.write_char(c)
} else {
buf.write_string("\\u{")
buf.write_string(u.to_string(radix=16))
buf.write_char('}')
}
}
buf.write_char('"')
buf.to_string()
}
///|
fn escape_lines(buf : StringBuilder, s : String) -> Unit {
for c in s {
match c {
'\n' => buf.write_string("\\n")
'\r' => buf.write_string("\\r")
'\t' => buf.write_string("\\t")
'\\' => buf.write_string("\\\\")
_ => buf.write_char(c)
}
}
}