///|
/// A structurally invalid AST, or an AST exceeding the serialization depth limit.
pub suberror SerializationError {
SerializationError(String)
}
///|
fn canonical_error(
expected : String,
node : Node,
) -> String raise SerializationError {
raise SerializationError("Expected " + expected + ", got " + node.type_name())
}
///|
fn canonical_depth(depth : Int32) -> Unit raise SerializationError {
// Arrays in synthetic nodes can refer back to their parents. Do not use
// structural equality here: it would itself recurse forever on such graphs.
if depth > 128 {
raise SerializationError(
"Serialization depth limit (128) exceeded; possible cyclic AST",
)
}
}
///|
/// Python str.splitlines(True) keeps all line separators, including CRLF.
/// Joining those lines with four spaces does not indent after a final separator.
fn canonical_indent(content : String) -> String {
let chars = content.to_array()
let out = StringBuilder()
let mut i = 0
while i < chars.length() {
let c = chars[i]
out.write_char(c)
i += 1
if c == '\r' && i < chars.length() && chars[i] == '\n' {
out.write_char('\n')
i += 1
}
if i < chars.length() &&
(
c == '\n' ||
c == '\r' ||
c == '\u000b' ||
c == '\u000c' ||
c == '\u001c' ||
c == '\u001d' ||
c == '\u001e' ||
c == '\u0085' ||
c == '\u2028' ||
c == '\u2029'
) {
out.write_string(" ")
}
}
out.to_string()
}
///|
fn canonical_id(node : Node) -> String raise SerializationError {
match node.kind {
Identifier(name) => name
_ => canonical_error("Identifier", node)
}
}
///|
fn canonical_comment(
node : Node,
prefix : String,
) -> String raise SerializationError {
let content = match node.kind {
Comment(content) | GroupComment(content) | ResourceComment(content) =>
content
_ => return canonical_error("Comment", node)
}
let out = StringBuilder()
for line in content.split("\n") {
out.write_string(prefix)
if !line.is_empty() {
out.write_char(' ')
out.write_string(line.to_owned())
}
out.write_char('\n')
}
out.to_string()
}
///|
fn canonical_pattern(
node : Node,
depth : Int32,
) -> String raise SerializationError {
canonical_depth(depth)
let elements = match node.kind {
Pattern(elements) => elements
_ => return canonical_error("Pattern", node)
}
let out = StringBuilder()
let mut multiline = false
for element in elements {
match element.kind {
TextElement(value) => {
out.write_string(value)
if value.contains("\n") {
multiline = true
}
}
Placeable(expr) => {
if expr.kind is SelectExpression(_, _) {
multiline = true
}
out.write_string(canonical_expression(element, depth + 1))
}
_ => return canonical_error("TextElement or Placeable", element)
}
}
if multiline && !elements.is_empty() {
match elements[0].kind {
TextElement(value) => {
let chars = value.to_array()
if chars.is_empty() {
raise SerializationError(
"Empty first TextElement in multiline Pattern",
)
}
if chars[0] == '[' || chars[0] == '.' || chars[0] == '*' {
multiline = false
}
}
_ => ()
}
}
(if multiline { "\n " } else { " " }) + canonical_indent(out.to_string())
}
///|
fn canonical_arguments(
node : Node,
depth : Int32,
) -> String raise SerializationError {
canonical_depth(depth)
let (positional, named) = match node.kind {
CallArguments(positional, named) => (positional, named)
_ => return canonical_error("CallArguments", node)
}
let parts : Array[String] = []
for arg in positional {
parts.push(canonical_expression(arg, depth + 1))
}
for arg in named {
match arg.kind {
NamedArgument(name, value) =>
parts.push(
canonical_id(name) + ": " + canonical_expression(value, depth + 1),
)
_ => return canonical_error("NamedArgument", arg)
}
}
"(" + parts.join(", ") + ")"
}
///|
fn canonical_variant(
node : Node,
depth : Int32,
) -> String raise SerializationError {
canonical_depth(depth)
match node.kind {
Variant(key, value, default) => {
let key = match key.kind {
Identifier(name) | NumberLiteral(name) => name
_ =>
return canonical_error("Identifier or NumberLiteral variant key", key)
}
"\n" +
(if default { " *" } else { " " }) +
"[" +
key +
"]" +
canonical_indent(canonical_pattern(value, depth + 1))
}
_ => canonical_error("Variant", node)
}
}
///|
fn canonical_expression(
node : Node,
depth : Int32,
) -> String raise SerializationError {
canonical_depth(depth)
match node.kind {
StringLiteral(value) => "\"" + value + "\""
NumberLiteral(value) => value
VariableReference(id) => "$" + canonical_id(id)
MessageReference(id, attribute) => {
let suffix = match attribute {
Some(attr) => "." + canonical_id(attr)
None => ""
}
canonical_id(id) + suffix
}
TermReference(id, attribute, arguments) => {
let attr = match attribute {
Some(attr) => "." + canonical_id(attr)
None => ""
}
let args = match arguments {
Some(args) => canonical_arguments(args, depth + 1)
None => ""
}
"-" + canonical_id(id) + attr + args
}
FunctionReference(id, arguments) =>
canonical_id(id) + canonical_arguments(arguments, depth + 1)
SelectExpression(selector, variants) => {
let out = StringBuilder()
out.write_string(canonical_expression(selector, depth + 1))
out.write_string(" ->")
for variant in variants {
out.write_string(canonical_variant(variant, depth + 1))
}
out.write_char('\n')
out.to_string()
}
Placeable(expr) => {
let value = canonical_expression(expr, depth + 1)
match expr.kind {
Placeable(_) => "{" + value + "}"
SelectExpression(_, _) => "{ " + value + "}"
_ => "{ " + value + " }"
}
}
_ => canonical_error("Expression or Placeable", node)
}
}
///|
fn canonical_attribute(
node : Node,
depth : Int32,
) -> String raise SerializationError {
canonical_depth(depth)
match node.kind {
Attribute(id, value) =>
"\n ." +
canonical_id(id) +
" =" +
canonical_indent(canonical_pattern(value, depth + 1))
_ => canonical_error("Attribute", node)
}
}
///|
fn canonical_entry(
node : Node,
has_entries : Bool,
depth : Int32,
) -> String raise SerializationError {
canonical_depth(depth)
match node.kind {
Message(id, value, attrs, comment) => {
let out = StringBuilder()
if comment is Some(comment) {
out.write_string(canonical_comment(comment, "#"))
}
out.write_string(canonical_id(id) + " =")
if value is Some(value) {
out.write_string(canonical_pattern(value, depth + 1))
}
for attr in attrs {
out.write_string(canonical_attribute(attr, depth + 1))
}
out.write_char('\n')
out.to_string()
}
Term(id, value, attrs, comment) => {
let out = StringBuilder()
if comment is Some(comment) {
out.write_string(canonical_comment(comment, "#"))
}
out.write_string("-" + canonical_id(id) + " =")
out.write_string(canonical_pattern(value, depth + 1))
for attr in attrs {
out.write_string(canonical_attribute(attr, depth + 1))
}
out.write_char('\n')
out.to_string()
}
Comment(_) | GroupComment(_) | ResourceComment(_) => {
let prefix = match node.kind {
GroupComment(_) => "##"
ResourceComment(_) => "###"
_ => "#"
}
(if has_entries { "\n" } else { "" }) +
canonical_comment(node, prefix) +
"\n"
}
Junk(content, _) => content
_ => canonical_error("Message, Term, Comment or Junk entry", node)
}
}
///|
/// Canonical python-fluent serialization. Spans and Junk annotations are ignored;
/// literal spellings are preserved rather than escaped or normalized again.
/// The root must be a Resource. Invalid child types and excessive nesting raise
/// SerializationError instead of silently dropping nodes or overflowing the stack.
pub fn serialize(
root : Node,
with_junk~ : Bool = false,
) -> String raise SerializationError {
let body = match root.kind {
Resource(body) => body
_ => return canonical_error("Resource", root)
}
let out = StringBuilder()
let mut has_entries = false
for entry in body {
if entry.kind is Junk(_, _) && !with_junk {
continue
}
out.write_string(canonical_entry(entry, has_entries, 1))
has_entries = true
}
out.to_string()
}