///|
pub struct Limits {
source : Int32
output : Int32
nodes : Int32
edges : Int32
alignments : Int32
depth : Int32
diagnostics : Int32
work : Int32
} derive(Eq, Debug)
///|
pub fn Limits::new(
source~ : Int32 = 16777216,
output~ : Int32 = 16777216,
nodes~ : Int32 = 10000,
edges~ : Int32 = 50000,
alignments~ : Int32 = 10000,
depth~ : Int32 = 256,
diagnostics~ : Int32 = 256,
work~ : Int32 = 1000000,
) -> Limits {
{ source, output, nodes, edges, alignments, depth, diagnostics, work, }
}
///|
pub struct ReadResult {
tree : @cst.Node
ast : @ast.Diagram?
diagnostics : ReadOnlyArray[@diagrams.Diagnostic]
} derive(Eq, Debug)
///|
pub let diagnostic_codes : ReadOnlyArray[String] = [
"architecture/syntax", "architecture/header", "architecture/source-limit", "architecture/output-limit",
"architecture/node-limit", "architecture/edge-limit", "architecture/alignment-limit",
"architecture/work-limit", "architecture/diagnostic-limit", "architecture/metadata-option",
]
///|
pub fn trim(value : String) -> String {
let mut a = 0
let mut b = value.length()
while a < b && [9, 10, 13, 32].contains(value.code_unit_at(a).to_int()) {
a += 1
}
while b > a && [9, 10, 13, 32].contains(value.code_unit_at(b - 1).to_int()) {
b -= 1
}
value[a:b].to_owned()
}
///|
priv struct Cursor {
text : String
mut at : Int32
mut valid : Bool
}
///|
fn code(c : Cursor) -> Int32 {
if c.at < c.text.length() {
c.text.code_unit_at(c.at).to_int()
} else {
0
}
}
///|
fn skip(c : Cursor) -> Unit {
while [9, 32].contains(code(c)) {
c.at += 1
}
}
///|
fn take(c : Cursor, value : String) -> Bool {
skip(c)
if c.text[c.at:].has_prefix(value) {
c.at += value.length()
true
} else {
false
}
}
///|
fn id(c : Cursor) -> String {
skip(c)
let start = c.at
let mut last = 0
while c.at < c.text.length() {
let n = code(c)
if (n >= 48 && n <= 57) ||
(n >= 65 && n <= 90) ||
(n >= 97 && n <= 122) ||
n == 95 ||
(n == 45 && c.at > start) {
c.at += 1
last = n
} else {
break
}
}
let name = c.text[start:c.at].to_owned()
if c.at == start ||
last == 45 ||
[
"L", "R", "T", "B", "group", "service", "junction", "align", "row", "column",
"in", "architecture-beta",
].contains(name) {
c.valid = false
}
name
}
///|
fn delimited(c : Cursor, open : Int32, close : Int32) -> String? {
skip(c)
if code(c) != open {
return None
}
c.at += 1
let start = c.at
let mut quote = 0
let mut escaped = false
while c.at < c.text.length() {
let n = code(c)
if escaped {
escaped = false
c.at += 1
continue
}
if n == 92 && quote != 0 {
escaped = true
c.at += 1
continue
}
if n == 34 || n == 39 {
if quote == n {
quote = 0
} else if quote == 0 {
quote = n
}
}
if n == close && quote == 0 {
let value = c.text[start:c.at].to_owned()
c.at += 1
if value == "" {
c.valid = false
}
if open == 40 &&
!value
.iter()
.all(ch => {
let n = ch.to_int()
(n >= 48 && n <= 57) ||
(n >= 65 && n <= 90) ||
(n >= 97 && n <= 122) ||
[95, 45, 58].contains(n)
}) {
c.valid = false
}
return Some(unquote(trim(value)))
}
if (n == 10 || n == 13 || n == open) && quote == 0 {
c.valid = false
return None
}
c.at += 1
}
c.valid = false
None
}
///|
pub fn unquote(value : String) -> String {
if value.length() >= 2 &&
(
(value.has_prefix("\"") && value.has_suffix("\"")) ||
(value.has_prefix("'") && value.has_suffix("'"))
) {
let value = value[1:value.length() - 1]
let out = StringBuilder()
let mut i = 0
while i < value.length() {
if value.code_unit_at(i).to_int() == 92 && i + 1 < value.length() {
i += 1
}
let n = value.code_unit_at(i).to_int()
let width = if n >= 55296 && n <= 56319 && i + 1 < value.length() {
2
} else {
1
}
out.write_string(value[i:i + width].to_owned())
i += width
}
out.to_string()
} else {
value
}
}
///|
fn quoted(c : Cursor) -> String? {
skip(c)
let q = code(c)
if q != 34 && q != 39 {
return None
}
let start = c.at
c.at += 1
let mut escaped = false
while c.at < c.text.length() {
let n = code(c)
c.at += 1
if escaped {
escaped = false
} else if n == 92 {
escaped = true
} else if n == q {
return Some(unquote(c.text[start:c.at].to_owned()))
}
}
c.valid = false
None
}
///|
fn parent(c : Cursor) -> String? {
if take(c, "in ") || take(c, "in\t") {
Some(id(c))
} else {
None
}
}
///|
fn side(c : Cursor) -> @model.Side? {
skip(c)
let n = code(c)
c.at += if n == 0 { 0 } else { 1 }
match n {
76 => Some(Left)
82 => Some(Right)
84 => Some(Top)
66 => Some(Bottom)
_ => {
c.valid = false
None
}
}
}
///|
fn statement(text : String) -> @ast.StatementKind? {
let c = Cursor::{ text, at: 0, valid: true, }
let kind = if take(c, "group ") || take(c, "group\t") {
let name = id(c)
let icon = delimited(c, 40, 41)
let label = delimited(c, 91, 93)
let p = parent(c)
Some(@ast.StatementKind::Group(id=name, icon~, label~, parent=p))
} else if take(c, "service ") || take(c, "service\t") {
let name = id(c)
let icon_text = quoted(c)
let icon = if icon_text == None { delimited(c, 40, 41) } else { None }
let label = delimited(c, 91, 93)
let p = parent(c)
Some(
@ast.StatementKind::Service(id=name, icon~, icon_text~, label~, parent=p),
)
} else if take(c, "junction ") || take(c, "junction\t") {
let name = id(c)
let p = parent(c)
Some(@ast.StatementKind::Junction(id=name, parent=p))
} else if take(c, "align ") || take(c, "align\t") {
let axis = if take(c, "row") {
Some(@model.Axis::Row)
} else if take(c, "column") {
Some(@model.Axis::Column)
} else {
None
}
let members = []
while c.at < c.text.length() && c.valid {
skip(c)
if c.at == c.text.length() {
break
}
members.push(id(c))
}
if axis is Some(axis) && members.length() >= 2 {
Some(
@ast.StatementKind::Alignment(
axis~,
members=ReadOnlyArray::from_array(members[:]),
),
)
} else {
None
}
} else {
let lhs = id(c)
let lhs_group = take(c, "{group}")
if !take(c, ":") {
return None
}
let lhs_side = side(c)
let source_head = take(c, "<") || take(c, ">")
if !take(c, "-") {
return None
}
let label = delimited(c, 91, 93)
if !take(c, "-") {
return None
}
let target_head = take(c, ">") || take(c, "<")
let rhs_side = side(c)
if !take(c, ":") {
return None
}
let rhs = id(c)
let rhs_group = take(c, "{group}")
match (lhs_side, rhs_side) {
(Some(a), Some(b)) =>
Some(
@ast.StatementKind::Edge(
source=@model.Endpoint::new(lhs, a, group=lhs_group),
target=@model.Endpoint::new(rhs, b, group=rhs_group),
label~,
source_head~,
target_head~,
),
)
_ => None
}
}
skip(c)
if !c.valid || c.at != text.length() {
None
} else {
kind
}
}
///|
pub fn read(source : String, limits~ : Limits = Limits::new()) -> ReadResult {
let tokens : Array[@cst.Token] = []
let statements : Array[@ast.Statement] = []
let diagnostics = []
let directives = []
let mut front_matter : String? = None
let mut header = false
let mut fatal = false
let mut nodes = 0
let mut edges = 0
let mut alignments = 0
let mut work = 0
fn error(code : String, text : String, start : Int32, end : Int32) {
if diagnostics.length() < limits.diagnostics.max(1) {
diagnostics.push(
@diagrams.Diagnostic::error(
"architecture/" + code,
text,
span=@positions.span(source, start, end),
),
)
} else {
fatal = true
diagnostics[diagnostics.length() - 1] = @diagrams.Diagnostic::error(
"architecture/diagnostic-limit", "Diagnostic budget exhausted",
)
}
}
if source.length() > limits.source || limits.source < 0 {
error("source-limit", "Source budget exhausted", 0, 0)
return {
tree: @cst.Node::new([{ kind: Error, text: source, offset: 0, }]),
ast: None,
diagnostics: ReadOnlyArray::from_array(diagnostics[:]),
}
}
let mut offset = 0
while offset < source.length() && !fatal {
let start = offset
let mut end = offset
let mut quote = 0
let mut escaped = false
let mut first_newline = None
let mut comment = false
let mut begin = start
while begin < source.length() &&
[9, 32].contains(source.code_unit_at(begin).to_int()) {
begin += 1
}
let plain = ["title ", "title\t", "accTitle", "accDescr", "%%"]
.iter()
.any(prefix => source[begin:].has_prefix(prefix))
while end < source.length() {
work += 1
if work > limits.work {
fatal = true
break
}
let n = source.code_unit_at(end).to_int()
if n == 10 {
if first_newline == None {
first_newline = Some(end)
}
if quote == 0 {
break
}
}
if plain || comment {
end += 1
continue
}
if n == 37 &&
end + 1 < source.length() &&
source.code_unit_at(end + 1).to_int() == 37 &&
quote == 0 {
comment = true
end += 1
continue
}
if escaped {
escaped = false
} else if n == 92 && quote != 0 {
escaped = true
} else if n == 34 || n == 39 {
if quote == n {
quote = 0
} else if quote == 0 {
quote = n
}
}
end += 1
}
if quote != 0 && end == source.length() {
if first_newline is Some(newline) {
end = newline
}
}
let scan_end = end
let mut text = trim(source[start:end].to_owned())
let mut token_kind : @cst.TokenKind = Statement
if text == "---" && !header {
let mut close = end + 1
while close < source.length() {
let mut e = close
while e < source.length() && source.code_unit_at(e).to_int() != 10 {
e += 1
}
if trim(source[close:e].to_owned()) == "---" {
end = e
break
}
close = e + 1
}
front_matter = Some(source[start:end].to_owned())
token_kind = FrontMatter
} else if text.has_prefix("%%{") {
match source[start:].find("}%%") {
Some(i) => {
end = start + i + 3
directives.push(source[start:end].to_owned())
token_kind = Directive
}
None => {
error("syntax", "Unterminated directive", start, end)
token_kind = Error
}
}
} else if text.has_prefix("%%") {
token_kind = Comment
statements.push({
kind: @ast.StatementKind::Comment(text),
span: @positions.span(source, start, end),
})
} else if text == "" {
token_kind = Whitespace
} else if !header {
if text == "architecture-beta" ||
text.has_prefix("architecture-beta ") ||
text.has_prefix("architecture-beta\t") {
header = true
token_kind = Header
let suffix = trim(text[17:].to_owned())
if suffix != "" {
match statement(suffix) {
Some(kind) => {
match kind {
Group(..) | Service(..) | Junction(..) => nodes += 1
Edge(..) => edges += 1
Alignment(..) => alignments += 1
_ => ()
}
statements.push({
kind,
span: @positions.span(source, start, end),
})
}
None =>
error(
"syntax", "Invalid statement after architecture header", start, end,
)
}
}
} else {
error("header", "Expected architecture-beta", start, end)
token_kind = Error
}
} else {
let mut comment_at = 0
let mut q = 0
let mut bracket = 0
while comment_at + 1 < text.length() {
let n = text.code_unit_at(comment_at).to_int()
if n == 34 || n == 39 {
if q == n {
q = 0
} else if q == 0 {
q = n
}
}
if q == 0 && n == 91 {
bracket += 1
}
if q == 0 && n == 93 {
bracket -= 1
}
if q == 0 && bracket == 0 && text[comment_at:].has_prefix("%%") {
text = trim(text[:comment_at].to_owned())
break
}
comment_at += 1
}
let kind = if text == "title" {
Some(@ast.StatementKind::Title(""))
} else if text.has_prefix("title ") || text.has_prefix("title\t") {
Some(@ast.StatementKind::Title(trim(text[5:].to_owned())))
} else if text.has_prefix("accTitle:") || text.has_prefix("accTitle :") {
let i = text.find(":").unwrap()
Some(@ast.StatementKind::AccTitle(trim(text[i + 1:].to_owned())))
} else if text.has_prefix("accDescr") {
if text.contains("{") {
let open = start + source[start:end].find("{").unwrap()
match source[open + 1:].find("}") {
Some(i) => {
end = open + 1 + i + 1
Some(
@ast.StatementKind::AccDescription(
trim(source[open + 1:end - 1].to_owned()),
),
)
}
None => None
}
} else {
match text.find(":") {
Some(i) =>
Some(
@ast.StatementKind::AccDescription(
trim(text[i + 1:].to_owned()),
),
)
None => None
}
}
} else {
statement(text)
}
match kind {
Some(kind) => {
match kind {
Group(..) | Service(..) | Junction(..) => nodes += 1
Edge(..) => edges += 1
Alignment(..) => alignments += 1
_ => ()
}
statements.push({ kind, span: @positions.span(source, start, end), })
}
None => {
error("syntax", "Invalid architecture statement", start, end)
token_kind = Error
}
}
}
work += (end - scan_end).max(0)
if work > limits.work {
error("work-limit", "Reader work budget exhausted", start, end)
fatal = true
}
if nodes > limits.nodes {
error("node-limit", "Node budget exhausted", start, end)
fatal = true
}
if edges > limits.edges {
error("edge-limit", "Edge budget exhausted", start, end)
fatal = true
}
if alignments > limits.alignments {
error("alignment-limit", "Alignment budget exhausted", start, end)
fatal = true
}
offset = if end < source.length() && source.code_unit_at(end).to_int() == 10 {
end + 1
} else {
end
}
tokens.push({
kind: token_kind,
text: source[start:offset].to_owned(),
offset: start,
})
}
if offset < source.length() {
tokens.push({ kind: Error, text: source[offset:].to_owned(), offset, })
}
if !header {
error("header", "Missing architecture-beta header", 0, 0)
}
{
tree: @cst.Node::new(tokens[:]),
ast: if fatal || !header {
None
} else {
Some(
@ast.Diagram::new(
statements[:],
front_matter?,
directives=directives[:],
),
)
},
diagnostics: ReadOnlyArray::from_array(diagnostics[:]),
}
}
///|
pub extend Limits with Eq::{equal, not_equal}
///|
pub extend Limits with @debug.Debug::{to_repr}
///|
pub extend ReadResult with Eq::{equal, not_equal}
///|
pub extend ReadResult with @debug.Debug::{to_repr}