///|
fn is_space(unit : UInt16) -> Bool {
unit == 32 || (unit >= 9 && unit <= 13)
}
///|
fn normalize(source : String) -> String {
let out = StringBuilder()
let mut start = 0
let mut pending_space = false
for i = 0; i <= source.length(); i = i + 1 {
if i == source.length() || is_space(source.code_unit_at(i)) {
if i > start {
if pending_space {
out.write_string(" ")
}
out.write_string(source[start:i].to_owned())
pending_space = true
}
start = i + 1
}
}
out.to_string()
}
///|
fn starts_word(text : String, word : String) -> Bool {
starts_word_at(text, 0, word)
}
///|
fn starts_word_at(text : String, at : Int32, word : String) -> Bool {
let end = at + word.length()
text.length() >= end &&
text[at:end].to_owned().to_lower() == word &&
(text.length() == end || text.code_unit_at(end) == 32)
}
///|
/// A word that starts a clause: a clause keyword, `then`, or a subject.
priv enum ClauseWord {
Clause(Keyword)
Then
SubjectWord
}
///|
fn clause_word(text : String, vocabulary : Vocabulary) -> ClauseWord? {
for keyword in keywords {
if starts_word(text, keyword.word()) {
return Some(Clause(keyword))
}
}
if starts_word(text, "then") {
Some(Then)
} else if vocabulary.starts_subject(text) {
Some(SubjectWord)
} else {
None
}
}
///|
fn starts_then_system(
text : String,
at : Int32,
vocabulary : Vocabulary,
) -> Bool {
starts_word_at(text, at, "then") &&
text.length() > at + 5 &&
vocabulary.starts_subject(text[at + 5:].to_owned())
}
///|
// A comma is structural only when a reserved clause keyword follows it.
// Ordinary commas in natural-language clauses remain part of that clause.
// With a dialect, a comma that a subject word follows ends the clause only
// when it is the last such comma before the modal verb, and an `If` clause
// ignores such commas, because it ends at `then`.
fn take_clause(
text : String,
unwanted : Bool,
vocabulary : Vocabulary,
at~ : SentencePosition,
) -> (String, String) raise ParseError {
let mut saw_incomplete_then = false
let mut subject_comma : Int32? = None
let split = (i : Int32) => {
(text[:i].trim_end().to_owned(), text[i + 1:].trim_start().to_owned())
}
for i in 0.. {
saw_incomplete_then = true
continue
}
Some(SubjectWord) if vocabulary.extended => {
if !unwanted {
subject_comma = Some(i)
}
continue
}
Some(_) => return split(i)
None => ()
}
}
if unwanted && text.code_unit_at(i) == 32 {
if starts_word_at(text, i + 1, "then") {
if starts_then_system(text, i + 1, vocabulary) {
return (text[:i].trim_end().to_owned(), text[i + 1:].to_owned())
}
saw_incomplete_then = true
}
}
}
match subject_comma {
Some(c) => return split(c)
None => ()
}
if unwanted {
if saw_incomplete_then {
raise ExpectedSystem(at~)
}
raise MissingThen(at~)
}
raise MissingSeparator(at~)
}
///|
/// The length of the lead-in phrase that starts `text`, followed by a space,
/// a comma or the end.
fn lead_in_at_start(text : String, vocabulary : Vocabulary) -> Int32? {
for phrase in vocabulary.lead_ins {
let n = phrase.length()
if text.length() >= n &&
text[:n].to_owned().to_lower() == phrase &&
(
text.length() == n ||
text.code_unit_at(n) == 32 ||
text.code_unit_at(n) == 44
) {
return Some(n)
}
}
None
}
///|
/// The lead-in phrase at the start of `text`, when a dialect gives one: up to
/// the first comma or space after the phrase that a clause keyword or a
/// subject follows, and never past the modal verb. Gives the lead-in and the
/// rest.
fn take_lead_in(text : String, vocabulary : Vocabulary) -> (String?, String) {
guard lead_in_at_start(text, vocabulary) is Some(length) else {
return (None, text)
}
for i in length.. Parsed raise ParseError {
let text = normalize(source)
guard !text.is_empty() else { raise EmptyRequirement }
// Each remaining text is a suffix of `text`, so its start is a position.
let position_of = (rest : String) => {
let p : SentencePosition = { offset: text.length() - rest.length(), }
p
}
let (lead_in, after_lead_in) = take_lead_in(text, vocabulary)
let mut rest = after_lead_in
let mut feature = None
let states = []
let mut trigger = None
while !vocabulary.starts_subject(rest) {
let at = position_of(rest)
let keyword = match clause_word(rest, vocabulary) {
Some(Clause(keyword)) => keyword
Some(Then | SubjectWord) => raise InvalidClauseOrder(at~)
None => raise ExpectedSystem(at~)
}
let in_order = match keyword {
Where => feature is None && states.is_empty() && trigger is None
While | When | If => trigger is None
}
guard in_order else { raise InvalidClauseOrder(at~) }
let payload = rest[keyword.word().length():].trim_start().to_owned()
let (clause, next) = take_clause(payload, keyword == If, vocabulary, at~)
guard !clause.is_empty() else { raise EmptyClause(keyword~, at~) }
match keyword {
Where => feature = Some(clause)
While => states.push(clause)
When => trigger = Some(Event(clause))
If => trigger = Some(Unwanted(clause))
}
rest = if keyword == If {
guard starts_word(next, "then") else { raise MissingThen(at~) }
let after_then = next[4:].trim_start().to_owned()
guard vocabulary.starts_subject(after_then) else {
raise ExpectedSystem(at=position_of(after_then))
}
after_then
} else {
next
}
}
let subject_at = position_of(rest)
// The subject, and the text from its modal verb on.
let (subject, from_modal) : (Subject, String) = match
vocabulary.subject_at_start(rest) {
Some(StartsNoun(n)) => {
let after = rest[n:].trim_start().to_owned()
guard vocabulary.modal_at(after, 0) is Some(_) else {
raise MissingShall(at=position_of(after))
}
(Noun(rest[:n].to_owned()), after)
}
Some(StartsPronoun(n)) => {
let after = rest[n:].trim_start().to_owned()
guard vocabulary.modal_at(after, 0) is Some(_) else {
raise MissingShall(at=position_of(after))
}
(Pronoun(opener={ text: rest[:n].to_owned(), }, resolved=None), after)
}
Some(StartsDeterminer(n)) => {
let body = rest[n:].trim_start().to_owned()
let mut modal_at = None
for i = 0; i < body.length(); i = i + 1 {
if (i == 0 || body.code_unit_at(i - 1) == 32) &&
vocabulary.modal_at(body, i) is Some(_) {
modal_at = Some(i)
break
}
}
guard modal_at is Some(at) else {
raise MissingShall(at=position_of(body))
}
let system = body[:at].trim_end().to_owned()
guard !system.is_empty() else {
raise ExpectedSystem(at=position_of(body))
}
(
Determined(opener={ text: rest[:n].to_owned(), }, system~),
body[at:].to_owned(),
)
}
None => raise ExpectedSystem(at=subject_at)
}
let verb_length = vocabulary.modal_at(from_modal, 0).unwrap_or(0)
let verb : Word = { text: from_modal[:verb_length].to_owned(), }
// Strict EARS keeps `not` in the response, as before dialects.
let negated = vocabulary.extended &&
starts_word_at(from_modal, verb_length + 1, "not")
let after_modal = if negated {
from_modal[verb_length + 4:].to_owned()
} else {
from_modal[verb_length:].to_owned()
}
let response = after_modal.trim_start().to_owned()
guard !response.is_empty() && response != "." else {
raise MissingResponse(at=position_of(after_modal))
}
{
requirement: {
lead_in,
feature,
preconditions: ReadOnlyArray::from_array(states[:]),
trigger,
subject,
modal: { verb, negated, },
response,
},
subject_at,
}
}
///|
/// Parse one sentence in strict EARS. Keywords are case-insensitive; clauses
/// use comma separators. IF requires THEN, with an optional comma before THEN.
/// Order: optional WHERE, zero or more WHILE, optional WHEN or IF, THE ... SHALL.
pub fn Requirement::Requirement(
source : String,
) -> Requirement raise ParseError {
parse_sentence(source, Vocabulary::strict()).requirement
}
///|
/// Parse one sentence with the vocabulary of `dialects`. Without a dialect,
/// it is the strict grammar of `Requirement::Requirement`.
pub fn parse(
source : String,
dialects~ : ArrayView[&Dialect] = [],
) -> Requirement raise ParseError {
parse_sentence(source, Vocabulary::of(dialects)).requirement
}