///|
fn quoted_text(text : String) -> String {
let out = StringBuilder()
out.write_string("\"")
for i = 0; i < text.length(); i = i + 1 {
let code = text[i]
if code == 34 || code == 92 {
out.write_string("\\")
}
out.write_stringview(text[i:i + 1])
}
out.write_string("\"")
out.to_string()
}
///|
fn render_atoms(atoms : Array[Atom]) -> String {
atoms
.map(atom => {
match atom {
Text(text) => quoted_text(text)
Word(text) => text
}
})
.join(" ")
}
///|
priv struct SyntaxWriter {
output : StringBuilder
limits : Limits
mut nodes : Int32
}
///|
priv struct WriterBudget {
limits : Limits
mut chars : Int32
mut nodes : Int32
}
///|
fn WriterBudget::add(
self : WriterBudget,
count : Int32,
pos : Position,
) -> Unit raise LibertyError {
if count > self.limits.max_input_chars - self.chars {
raise failure("InputLimit", "rendered syntax exceeds character budget", pos)
}
self.chars += count
}
///|
fn WriterBudget::atoms(
self : WriterBudget,
atoms : Array[Atom],
pos : Position,
) -> Unit raise LibertyError {
for i = 0; i < atoms.length(); i = i + 1 {
if i > 0 {
self.add(1, pos)
}
let text = atom_text(atoms[i])
self.add(text.length(), pos)
if atoms[i] is Text(_) {
self.add(2, pos)
for j = 0; j < text.length(); j = j + 1 {
if text[j] == 34 || text[j] == 92 {
self.add(1, pos)
}
}
}
}
}
///|
fn WriterBudget::arguments(
self : WriterBudget,
args : Array[Array[Atom]],
pos : Position,
) -> Unit raise LibertyError {
for i = 0; i < args.length(); i = i + 1 {
if i > 0 {
self.add(2, pos)
}
self.atoms(args[i], pos)
}
}
///|
fn WriterBudget::group(
self : WriterBudget,
group : Group,
depth : Int32,
) -> Unit raise LibertyError {
if depth >= self.limits.max_depth {
raise failure("DepthLimit", "writer nesting exceeds limit", group.position)
}
self.nodes += 1
if self.nodes > self.limits.max_nodes {
raise failure("NodeLimit", "writer node budget exceeded", group.position)
}
self.add(depth * 2, group.position)
self.add(group.kind.length(), group.position)
self.add(6, group.position)
self.arguments(group.arguments, group.position)
for item in group.items {
match item {
Section(child) => self.group(child, depth + 1)
Attribute(name, _, pos) | Complex(name, _, pos) => {
self.nodes += 1
if self.nodes > self.limits.max_nodes {
raise failure("NodeLimit", "writer node budget exceeded", pos)
}
self.add((depth + 1) * 2, pos)
self.add(name.length(), pos)
self.add(5, pos)
match item {
Attribute(_, values, _) => self.atoms(values, pos)
Complex(_, args, _) => self.arguments(args, pos)
_ => ()
}
}
}
}
self.add(depth * 2, group.position)
self.add(2, group.position)
}
///|
fn SyntaxWriter::indent(self : SyntaxWriter, depth : Int32) -> Unit {
for i = 0; i < depth; i = i + 1 {
self.output.write_string(" ")
}
}
///|
fn SyntaxWriter::group(
self : SyntaxWriter,
group : Group,
depth : Int32,
) -> Unit raise LibertyError {
if depth >= self.limits.max_depth {
raise failure("DepthLimit", "writer nesting exceeds limit", group.position)
}
self.nodes += 1
if self.nodes > self.limits.max_nodes {
raise failure("NodeLimit", "writer node budget exceeded", group.position)
}
self.indent(depth)
self.output.write_string(
group.kind + " (" + group.arguments.map(render_atoms).join(", ") + ") {\n",
)
for item in group.items {
match item {
Section(child) => self.group(child, depth + 1)
Attribute(name, atoms, pos) => {
self.nodes += 1
if self.nodes > self.limits.max_nodes {
raise failure("NodeLimit", "writer node budget exceeded", pos)
}
self.indent(depth + 1)
self.output.write_string(name + " : " + render_atoms(atoms) + ";\n")
}
Complex(name, arguments, pos) => {
self.nodes += 1
if self.nodes > self.limits.max_nodes {
raise failure("NodeLimit", "writer node budget exceeded", pos)
}
self.indent(depth + 1)
self.output.write_string(
name + " (" + arguments.map(render_atoms).join(", ") + ");\n",
)
}
}
}
self.indent(depth)
self.output.write_string("}\n")
}
///|
pub fn write_liberty(
group : Group,
limits~ : Limits = Limits::default(),
) -> String raise LibertyError {
// Validate budgets before allocating the rendered text, including escaping
// and indentation. Caller-constructed ASTs need not originate in the lexer.
ignore(tokenize("", limits~))
let budget : WriterBudget = { limits, chars: 0, nodes: 0, }
budget.group(group, 0)
let writer : SyntaxWriter = { output: StringBuilder(), limits, nodes: 0, }
writer.group(group, 0)
let output = writer.output.to_string()
// Also validate caller-constructed ASTs and enforce output character budgets.
let reparsed = parse_liberty(output, limits~)
if !same_syntax(group, reparsed) {
raise failure(
"InvalidAst",
"writer would change caller-constructed syntax",
group.position,
)
}
output
}
///|
fn same_syntax(before : Group, after : Group) -> Bool {
if before.kind != after.kind ||
before.arguments != after.arguments ||
before.items.length() != after.items.length() {
return false
}
for i in 0.. a == b && x == y
(Complex(a, x, _), Complex(b, y, _)) => a == b && x == y
(Section(a), Section(b)) => same_syntax(a, b)
_ => false
}
if !equal {
return false
}
}
true
}