///|
pub struct Limits {
source : Int32
output : Int32
participants : Int32
events : Int32
depth : Int32
diagnostics : Int32
work : Int32
} derive(Eq, Debug)
///|
pub fn Limits::new(
source~ : Int32 = 16777216,
output~ : Int32 = 16777216,
participants~ : Int32 = 10000,
events~ : Int32 = 50000,
depth~ : Int32 = 256,
diagnostics~ : Int32 = 256,
work~ : Int32 = 1000000,
) -> Limits {
{ source, output, participants, events, depth, diagnostics, work, }
}
///|
pub struct ReadResult {
tree : @cst.Node
ast : @ast.Diagram?
diagnostics : ReadOnlyArray[@diagrams.Diagnostic]
} derive(Eq, Debug)
///|
fn ch(s : String, i : Int32) -> Int32 {
if i >= 0 && i < s.length() {
s.at(i).to_int()
} else {
0
}
}
///|
fn trim(s : String) -> String {
s.trim().to_owned()
}
///|
fn cut(s : String, a : Int32, b : Int32) -> String {
s[a:b].to_owned()
}
///|
fn find(s : String, part : String) -> Int32? {
s.find(part)
}
///|
fn find_top(s : String, token : String) -> Int32? {
let mut quote = 0
let mut escape = false
let mut depth = 0
let mut i = 0
while i < s.length() {
let c = ch(s, i)
if quote != 0 {
if escape {
escape = false
} else if c == 92 {
escape = true
} else if c == quote {
quote = 0
}
i += 1
continue
}
if c == 34 || c == 39 || c == 96 {
quote = c
i += 1
continue
}
if depth == 0 && s[i:].has_prefix(token) {
return Some(i)
}
if c == 40 || c == 91 {
depth += 1
}
if c == 41 || c == 93 {
depth = max(0, depth - 1)
}
i += 1
}
None
}
///|
fn unquote(s : String) -> String {
if s.length() >= 2 &&
(
(s.has_prefix("\"") && s.has_suffix("\"")) ||
(s.has_prefix("'") && s.has_suffix("'"))
) {
cut(s, 1, s.length() - 1).replace_all(old="\"\"", new="\"")
} else {
s
}
}
///|
fn identifier(s : String) -> Bool {
s.length() > 0 &&
s
.iter()
.all(c => {
((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) ||
c == '_' ||
c == '$'
})
}
///|
/// A bounded scanner separates braces only outside strings, comments and expressions.
pub fn read(source : String, limits~ : Limits = Limits::new()) -> ReadResult {
let line_starts = [0]
if source.length() <= limits.source && source.length() <= limits.work {
let mut at = 0
while at < source.length() {
let c = ch(source, at)
if c == 13 && ch(source, at + 1) == 10 {
at += 2
line_starts.push(at)
} else {
at += 1
if c == 10 || c == 13 {
line_starts.push(at)
}
}
}
}
fn position(offset : Int32) -> @diagrams.Position {
let mut left = 0
let mut right = line_starts.length()
while left + 1 < right {
let middle = (left + right) / 2
if line_starts[middle] <= offset {
left = middle
} else {
right = middle
}
}
{ offset, line: left + 1, column: offset - line_starts[left] + 1, }
}
fn span(a : Int32, b : Int32) -> @diagrams.Span {
{ start: position(a), end: position(b), }
}
let tree = @cst.Node::new(source)
let diagnostics = []
let statements : Array[@ast.Statement] = []
let opens : Array[(@diagrams.Span, String)] = []
let mut failed = false
let mut work = 0
let mut count = 0
let mut declarations = 0
let mut annotation : String? = None
let mut reply = false
fn error(code : String, message : String, a : Int32, b : Int32) {
if diagnostics.length() < max(1, limits.diagnostics) {
diagnostics.push(
@diagrams.Diagnostic::error("zenuml/" + code, message, span=span(a, b)),
)
} else {
failed = true
diagnostics[diagnostics.length() - 1] = @diagrams.Diagnostic::error(
"zenuml/diagnostic-limit",
"Diagnostic limit exceeded",
span=span(a, b),
)
}
}
fn append(kind : @ast.StatementKind, a : Int32, b : Int32) {
if kind is Participant(..) {
declarations += 1
if declarations > limits.participants {
error(
"participant-limit", "Participant declaration limit exceeded", a, b,
)
failed = true
return
}
}
statements.push({ kind, span: span(a, b), })
count += 1
}
if limits.source < 0 ||
limits.events < 0 ||
limits.depth < 0 ||
limits.depth > 256 ||
limits.work < 0 ||
limits.diagnostics < 1 ||
source.length() > limits.source ||
source.length() > limits.work {
error(
if source.length() > limits.work {
"work-limit"
} else {
"source-limit"
},
"Invalid limits or source/work limit exceeded",
0,
0,
)
return {
tree,
ast: None,
diagnostics: ReadOnlyArray::from_array(diagnostics),
}
}
let tokens : Array[(String, Int32, Int32, Bool)] = []
let mut start = 0
let mut i = 0
let mut quote = 0
let mut escape = false
let mut parens = 0
let mut async_text = false
while i < source.length() {
work += 1
if work > limits.work {
error("work-limit", "Scanner work limit exceeded", i, i)
failed = true
break
}
let c = ch(source, i)
if i == 0 && c == 65279 {
i += 1
start = i
continue
}
// EVENT_PAYLOAD_LXR consumes everything after an async colon to EOL.
// Payload punctuation cannot reopen the statement scanner's lexical modes.
if async_text {
if c == 10 || c == 13 {
tokens.push((trim(cut(source, start, i)), start, i, false))
async_text = false
parens = 0
i += 1
start = i
} else {
i += 1
}
continue
}
if quote != 0 && (c == 10 || c == 13) {
error(
"unterminated-string", "Quoted statement does not close on this line", start,
i,
)
quote = 0
escape = false
}
if quote != 0 {
if escape {
escape = false
} else if c == 92 {
escape = true
} else if c == quote {
quote = 0
}
i += 1
continue
}
if i == start &&
source[i:].has_prefix("---") &&
(ch(source, i + 3) == 10 || ch(source, i + 3) == 13) {
let a = i
i += 3
while i < source.length() {
if (ch(source, i - 1) == 10 || ch(source, i - 1) == 13) &&
source[i:].has_prefix("---") {
i += 3
break
}
i += 1
}
tokens.push((cut(source, a, i), a, i, false))
work += i - a
start = i
continue
}
if c == 37 && ch(source, i + 1) == 37 {
if trim(cut(source, start, i)) != "" {
tokens.push((trim(cut(source, start, i)), start, i, false))
}
let a = i
if ch(source, i + 2) == 123 {
i += 3
while i < source.length() && !source[i:].has_prefix("}%%") {
i += 1
}
if i < source.length() {
i += 3
}
} else {
while i < source.length() && ch(source, i) != 10 && ch(source, i) != 13 {
i += 1
}
}
tokens.push((cut(source, a, i), a, i, false))
work += i - a
start = i
continue
}
if c == 34 || c == 39 || c == 96 {
quote = c
i += 1
continue
}
if c == 47 && ch(source, i + 1) == 47 {
if trim(cut(source, start, i)) != "" {
tokens.push((trim(cut(source, start, i)), start, i, false))
}
let a = i
while i < source.length() && ch(source, i) != 10 && ch(source, i) != 13 {
i += 1
}
tokens.push((cut(source, a, i), a, i, false))
start = i
continue
}
if c == 58 && parens == 0 && cut(source, start, i).contains("->") {
async_text = true
}
if (c == 10 || c == 13) && parens > 0 {
let mut next = i + 1
if c == 13 && ch(source, next) == 10 {
next += 1
}
let mut end = next
while end < source.length() &&
ch(source, end) != 10 &&
ch(source, end) != 13 {
end += 1
}
let next_line = trim(cut(source, next, end))
let message_boundary = match find_top(next_line, "->") {
Some(arrow) => identifier(trim(cut(next_line, 0, arrow)))
None => false
}
if message_boundary {
error(
"unclosed-expression", "Expression does not close before the next message",
start, i,
)
parens = 0
}
}
if !async_text {
if c == 40 {
parens += 1
}
if c == 41 {
parens = max(0, parens - 1)
}
}
if ((c == 123 || c == 125) && parens == 0 && !async_text) ||
((c == 10 || c == 13) && (parens == 0 || async_text)) ||
(c == 59 && parens == 0) {
let text = trim(cut(source, start, i))
if text != "" {
tokens.push((text, start, i, c == 123))
}
if c == 125 {
tokens.push(("}", i, i + 1, false))
}
if c == 10 || c == 13 {
parens = 0
async_text = false
}
i += 1
start = i
continue
}
i += 1
}
if quote != 0 {
error(
"unterminated-string",
"Quoted statement has no closing quote",
start,
source.length(),
)
}
if parens > 0 {
error(
"unclosed-expression",
"Expression has no closing parenthesis",
start,
source.length(),
)
}
if start < source.length() {
let text = trim(cut(source, start, source.length()))
if text != "" {
tokens.push((text, start, source.length(), false))
}
}
let mut header = false
for (raw, begin, b, scope) in tokens {
let mut a = begin
while a < b && [9, 10, 13, 32].contains(ch(source, a)) {
a += 1
}
work += raw.length() + 1
if work > limits.work || count >= limits.events {
error(
if work > limits.work {
"work-limit"
} else {
"event-limit"
},
"Statement processing budget exceeded",
a,
b,
)
failed = true
break
}
let text = trim(raw)
if text == "zenuml" {
if header {
error("header", "Duplicate header", a, b)
}
header = true
continue
}
if text.has_prefix("---") || text.has_prefix("%%") {
append(Metadata(text), a, b)
continue
}
if !header {
error("header", "Expected zenuml header", a, b)
continue
}
if text.has_prefix("//") {
append(Comment(trim(cut(text, 2, text.length()))), a, b)
continue
}
if text == "}" {
if opens.pop() is None {
error(
"unexpected-brace", "Closing brace has no opening statement", a, b,
)
} else {
append(End, a, b)
}
continue
}
if text.has_prefix("title ") {
append(Title(trim(cut(text, 6, text.length()))), a, b)
continue
}
let mut text = text
if text.has_prefix("@Starter(") || text.has_prefix("@starter(") {
if text.has_suffix(")") {
append(Starter(trim(cut(text, 9, text.length() - 1))), a, b)
} else {
error("starter", "Malformed starter declaration", a, b)
}
continue
}
if ["@return", "@reply", "@Return", "@Reply"]
.iter()
.any(key => {
text == key || text.has_prefix(key + " ") || text.has_prefix(key + "\t")
}) {
let n = if text.has_prefix("@return") || text.has_prefix("@Return") {
7
} else {
6
}
reply = true
text = trim(cut(text, n, text.length()))
if text == "" {
continue
}
} else if text.has_prefix("@") {
let mut end = 1
while end < text.length() && ch(text, end) != 32 && ch(text, end) != 9 {
end += 1
}
annotation = Some(cut(text, 1, end))
text = trim(cut(text, end, text.length()))
if text == "" {
continue
}
}
if text.has_prefix("return") && (text == "return" || ch(text, 6) == 32) {
append(Return(trim(cut(text, 6, text.length()))), a, b)
continue
}
let words = [
"else if", "else", "catch", "finally", "while", "forEach", "foreach", "for",
"loop", "if", "opt", "par", "try", "break",
]
let fragment = words
.iter()
.find_first(word => {
text == word ||
text.has_prefix(word + "(") ||
text.has_prefix(word + " ")
})
if fragment is Some(keyword) {
if !scope {
error("expected-brace", "Control statement requires a body", a, b)
continue
}
let expression = trim(cut(text, keyword.length(), text.length()))
append(
if ["else if", "else", "catch", "finally"].contains(keyword) {
Section(keyword~, expression~)
} else {
Begin(keyword~, expression~)
},
a,
b,
)
opens.push((span(a, b), keyword))
if opens.length() > limits.depth {
error("depth-limit", "Nesting depth limit exceeded", a, b)
failed = true
break
}
continue
}
if find_top(text, "->") is Some(arrow) {
let sender = trim(cut(text, 0, arrow))
let tail = trim(cut(text, arrow + 2, text.length()))
if find_top(tail, ":") is Some(colon) {
let receiver = trim(cut(tail, 0, colon))
if !identifier(sender) || !identifier(receiver) {
error("identifier", "Invalid message endpoint", a, b)
} else {
append(
Async(
sender~,
receiver~,
label=trim(cut(tail, colon + 1, tail.length())),
reply~,
),
a,
b,
)
}
reply = false
continue
}
}
let mut assignment = None
let mut call = text
if find_top(call, "=") is Some(eq) {
assignment = Some(trim(cut(call, 0, eq)))
call = trim(cut(call, eq + 1, call.length()))
}
let mut sender = None
if find_top(call, "->") is Some(arrow) {
sender = Some(trim(cut(call, 0, arrow)))
call = trim(cut(call, arrow + 2, call.length()))
}
let creation = call.has_prefix("new ")
if creation {
call = trim(cut(call, 4, call.length()))
}
if creation {
let end = find(call, "(").unwrap_or(call.length())
let receiver = trim(cut(call, 0, end))
let operation = cut(call, end, call.length())
if !identifier(receiver) {
error("identifier", "Invalid created participant", a, b)
} else {
append(
Call(sender~, receiver~, operation~, assignment~, creation~, scope~),
a,
b,
)
}
if scope {
opens.push((span(a, b), "call"))
}
} else if find_top(call, ".") is Some(dot) {
let receiver = trim(cut(call, 0, dot))
let operation = trim(cut(call, dot + 1, call.length()))
if !identifier(receiver) || operation == "" {
error("call", "Invalid synchronous call", a, b)
} else {
append(
Call(sender~, receiver~, operation~, assignment~, creation~, scope~),
a,
b,
)
}
if scope {
opens.push((span(a, b), "call"))
}
} else {
let label = find_top(text, " as ")
let id = trim(if label is Some(end) { cut(text, 0, end) } else { text })
let label = label.map(end => {
unquote(trim(cut(text, end + 4, text.length())))
})
if identifier(id) && !scope && assignment == None {
append(Participant(id~, label~, annotator=annotation), a, b)
annotation = None
} else {
error(
"unsupported-statement",
"Unsupported or malformed statement: " + text,
a,
b,
)
}
}
if opens.length() > limits.depth {
error("depth-limit", "Nesting depth limit exceeded", a, b)
failed = true
break
}
}
if annotation is Some(_) {
error(
"identifier",
"Participant annotator has no participant",
source.length(),
source.length(),
)
}
if reply {
error(
"unsupported-statement",
"Reply annotator has no asynchronous message",
source.length(),
source.length(),
)
}
if !header {
error("header", "Expected zenuml header", 0, 0)
}
for (span, _) in opens {
if diagnostics.length() < limits.diagnostics {
diagnostics.push(
@diagrams.Diagnostic::error(
"zenuml/missing-brace",
"Opening statement has no closing brace",
span~,
),
)
}
}
{
tree,
ast: if failed {
None
} else {
Some(@ast.Diagram::new(statements))
},
diagnostics: ReadOnlyArray::from_array(diagnostics),
}
}
///|
fn max(a : Int32, b : Int32) -> Int32 {
if a > b {
a
} else {
b
}
}
///|
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}
///|
/// Stable diagnostic codes emitted by this processing stage.
pub let diagnostic_codes : ReadOnlyArray[String] = [
"zenuml/call", "zenuml/depth-limit", "zenuml/diagnostic-limit", "zenuml/event-limit",
"zenuml/expected-brace", "zenuml/header", "zenuml/identifier", "zenuml/missing-brace",
"zenuml/participant-limit", "zenuml/source-limit", "zenuml/starter", "zenuml/unclosed-expression",
"zenuml/unexpected-brace", "zenuml/unsupported-statement", "zenuml/unterminated-string",
"zenuml/work-limit",
]