///|
priv struct Printer {
limits : @envelope.Limits
mut used : Int
mut work : Int
mut failure : @diagrams.Diagnostic?
}
///|
fn Printer::fail(self : Printer, message : String) -> String {
if self.failure is None {
self.failure = Some(
@diagrams.Diagnostic::error(
"ebnf/unrepresentable",
"Cannot write the grammar: " + message,
),
)
}
""
}
///|
fn Printer::charge(self : Printer, text : String) -> String {
self.work += 1
if self.work > self.limits.work || text.length() > self.limits.output - self.used {
if self.failure is None {
self.failure = Some(
@diagrams.Diagnostic::error("ebnf/limit", "EBNF output limit exceeded"),
)
}
return ""
}
self.used += text.length()
text
}
///|
fn note_text(p : Printer, text : String) -> String {
if text.contains("*)") || text.contains("\n") {
p.fail("a note on a term cannot contain '*)' or a line break")
} else {
"(* " + text + " *)"
}
}
///|
fn leaf(p : Printer, body : String, notes : Notes) -> String {
let above = notes.above.map(t => note_text(p, t) + " ").unwrap_or("")
let below = notes.below.map(t => " " + note_text(p, t)).unwrap_or("")
p.charge(above + body + below)
}
///|
fn valid_name(name : String) -> Bool {
!name.is_empty() &&
name.code_unit_at(0).to_int() != 45 &&
name.iter().all(c => literal_char(c.to_int()))
}
///|
/// Levels: 0 alternation, 1 concatenation, 2 count, 3 primary.
fn expr_text(p : Printer, e : Expr, level : Int, depth : Int) -> String {
if depth > p.limits.depth {
return p.fail("the expression is too deep")
}
if p.failure is Some(_) {
return ""
}
match e {
Terminal(text, quote, notes, _) => {
let q = if quote == Double { "\"" } else { "'" }
if text.contains(q) {
return p.fail("a terminal cannot contain its own quote character")
}
leaf(p, q + text + q, notes)
}
Name(name, notes, _) =>
if valid_name(name) {
leaf(p, name, notes)
} else {
p.fail("'\{name}' is not a valid rule name")
}
Special(text, notes, _) =>
if text.contains("?") {
p.fail("a special sequence cannot contain '?'")
} else {
leaf(p, "?" + text + "?", notes)
}
Concat(items) => {
if items.length() < 2 {
return p.fail("a concatenation needs two items")
}
let units : Array[String] = []
for item in items {
match item {
Not(excluded, _) => {
guard units.pop() is Some(left) else {
return p.fail("an exclusion needs a term on its left")
}
units.push("(" + left + " - " + expr_text(p, excluded, 0, depth + 1) + ")")
}
Concat(_) => return p.fail("a nested concatenation is not flat")
_ => units.push(expr_text(p, item, 1, depth + 1))
}
}
let text = units.join(", ")
if level > 1 {
"(" + text + ")"
} else {
text
}
}
Alt(items) => {
if items.length() < 2 {
return p.fail("an alternation needs two alternatives")
}
let parts = []
for item in items {
match item {
Alt(_) => return p.fail("a nested alternation is not flat")
Not(_, _) => return p.fail("an exclusion must follow a term")
_ => parts.push(expr_text(p, item, 1, depth + 1))
}
}
let text = parts.join(" | ")
if level > 0 {
"(" + text + ")"
} else {
text
}
}
Optional(inner, _) => "[ " + expr_text(p, inner, 0, depth + 1) + " ]"
ZeroOrMore(inner, _) => "{ " + expr_text(p, inner, 0, depth + 1) + " }"
OneOrMore(inner, _) => "{ " + expr_text(p, inner, 0, depth + 1) + " }-"
Count(count, inner, _) => {
if !is_decimal(count) {
return p.fail("a count must be a decimal number")
}
let text = count + " * " + expr_text(p, inner, 3, depth + 1)
if level > 2 {
"(" + text + ")"
} else {
text
}
}
Not(_, _) => p.fail("an exclusion must follow a term")
}
}
///|
let statement_words : Array[String] = [
"title", "caption", "header", "footer", "legend", "skinparam", "skin", "scale", "hide",
"show", "sprite", "rotate", "page", "minwidth", "mainframe",
]
///|
/// The canonical body of an EBNF document: one line per rule and per note
/// line, derived from typed values.
pub fn print_body(
document : Document,
limits? : @envelope.Limits = @envelope.Limits::new(),
) -> Result[String, ReadOnlyArray[@diagrams.Diagnostic]] {
let p = Printer::{ limits, used: 0, work: 0, failure: None }
let out = StringBuilder()
for item in document.items {
match item {
Note(lines, _) => {
if lines.is_empty() {
ignore(p.fail("a note needs a line"))
}
for i, line in lines {
let ends = line.has_suffix("*)")
if line.contains("\n") || line != @envelope.trim(line) {
ignore(p.fail("a note line cannot have a line break or outer spaces"))
}
if lines.length() == 1 {
if line.contains("*)") {
ignore(p.fail("a single-line note cannot contain '*)'"))
}
out.write_string(p.charge("(* " + line + " *)\n"))
} else if i == 0 {
if ends {
ignore(p.fail("a note line cannot end with '*)'"))
}
out.write_string(p.charge("(* " + line + "\n"))
} else if i == lines.length() - 1 {
out.write_string(p.charge(line + " *)\n"))
} else {
if ends {
ignore(p.fail("a note line cannot end with '*)'"))
}
out.write_string(p.charge(line + "\n"))
}
}
}
Rule(rule) => {
if !valid_name(rule.name) {
ignore(p.fail("'\{rule.name}' is not a valid rule name"))
}
if statement_words.contains(rule.name) {
ignore(p.fail("a rule named '\{rule.name}' reads as a statement"))
}
if rule.above is Some(_) {
ignore(p.fail("a note above a rule name cannot be written reliably"))
}
let below = match rule.below {
Some(text) => " " + note_text(p, text)
None => ""
}
let body = match rule.body {
Some(e) => " " + expr_text(p, e, 0, 0)
None => " "
}
let line = rule.name + below + " =" + body + ";"
if @envelope.has_preprocessor_call(line) {
ignore(p.fail("the rule text reads as a preprocessor call"))
}
out.write_string(p.charge(line + "\n"))
}
}
}
match p.failure {
Some(d) => Err([d])
None => Ok(out.to_string())
}
}