pub(super) struct GoDirective {
pub(super) verb: Box<str>,
pub(super) arguments: Box<[Box<str>]>,
pub(super) line: u32,
}
pub(super) struct GoDirectiveError {
pub(super) line: u32,
pub(super) message: Box<str>,
}
pub(super) const ARROW: &str = "=>";
pub(super) fn parse(text: &str) -> Result<Box<[GoDirective]>, GoDirectiveError> {
let mut directives: Vec<GoDirective> = Vec::new();
let mut block: Option<(Box<str>, u32)> = None;
for (index, raw) in text.lines().enumerate() {
let line = line_number(index);
let tokens = tokenize(raw, line)?;
block = fold_line(&mut directives, block, (tokens, line))?;
}
match block {
Some((verb, line)) => Err(GoDirectiveError {
line,
message: format!("the {verb} block is never closed").into_boxed_str(),
}),
None => Ok(directives.into_boxed_slice()),
}
}
type BlockState = Option<(Box<str>, u32)>;
fn fold_line(
directives: &mut Vec<GoDirective>,
block: BlockState,
line: (Box<[Box<str>]>, u32),
) -> Result<BlockState, GoDirectiveError> {
match block {
Some(open) => Ok(fold_inside_block(directives, open, line)),
None => fold_at_top_level(directives, line),
}
}
fn fold_inside_block(
directives: &mut Vec<GoDirective>,
open: (Box<str>, u32),
line: (Box<[Box<str>]>, u32),
) -> BlockState {
let (verb, opened) = open;
let (tokens, number) = line;
let (arguments, closed) = split_close(tokens.into_vec());
record(directives, (&verb, arguments), number);
match closed {
true => None,
false => Some((verb, opened)),
}
}
fn fold_at_top_level(
directives: &mut Vec<GoDirective>,
line: (Box<[Box<str>]>, u32),
) -> Result<BlockState, GoDirectiveError> {
let (tokens, number) = line;
let mut tokens = tokens.into_vec();
let Some(verb) = held_verb(&mut tokens, number)? else {
return Ok(None);
};
let opens = first_is(&tokens, "(");
let body = tokens.split_off(usize::from(opens));
let (arguments, closed) = split_close(body);
check_close(opens, closed, number)?;
record(directives, (&verb, arguments), number);
Ok(match opens && !closed {
true => Some((verb, number)),
false => None,
})
}
fn held_verb(tokens: &mut Vec<Box<str>>, line: u32) -> Result<Option<Box<str>>, GoDirectiveError> {
let Some(verb) = pop_first(tokens) else {
return Ok(None);
};
match &*verb == ")" {
true => Err(closed_unopened(line)),
false => Ok(Some(verb)),
}
}
fn pop_first(tokens: &mut Vec<Box<str>>) -> Option<Box<str>> {
match tokens.is_empty() {
true => None,
false => Some(tokens.remove(0)),
}
}
fn check_close(opens: bool, closed: bool, line: u32) -> Result<(), GoDirectiveError> {
match closed && !opens {
false => Ok(()),
true => Err(closed_unopened(line)),
}
}
fn closed_unopened(line: u32) -> GoDirectiveError {
GoDirectiveError {
line,
message: Box::from("a block is closed without being opened"),
}
}
fn split_close(tokens: Vec<Box<str>>) -> (Vec<Box<str>>, bool) {
let mut arguments = tokens;
match arguments.last().is_some_and(|last| &**last == ")") {
false => (arguments, false),
true => {
arguments.pop();
(arguments, true)
}
}
}
fn first_is(tokens: &[Box<str>], token: &str) -> bool {
tokens.first().is_some_and(|held| &**held == token)
}
fn record(directives: &mut Vec<GoDirective>, entry: (&str, Vec<Box<str>>), line: u32) {
let (verb, arguments) = entry;
match arguments.is_empty() {
true => (),
false => directives.push(GoDirective {
verb: verb.into(),
arguments: arguments.into_boxed_slice(),
line,
}),
}
}
fn tokenize(raw: &str, line: u32) -> Result<Box<[Box<str>]>, GoDirectiveError> {
let mut tokens: Vec<Box<str>> = Vec::new();
let mut current = String::new();
let mut quoted = false;
let mut characters = raw.chars().peekable();
while let Some(character) = characters.next() {
match (quoted, character) {
(true, '"') => quoted = false,
(true, _) => current.push(character),
(false, '"') => quoted = true,
(false, '/') if characters.peek() == Some(&'/') => break,
(false, _) if character.is_whitespace() => flush(&mut tokens, &mut current),
(false, _) => current.push(character),
}
}
match quoted {
true => Err(GoDirectiveError {
line,
message: Box::from("the line ends inside a quoted token"),
}),
false => {
flush(&mut tokens, &mut current);
Ok(tokens.into_boxed_slice())
}
}
}
fn flush(tokens: &mut Vec<Box<str>>, current: &mut String) {
match current.is_empty() {
true => (),
false => tokens.push(std::mem::take(current).into_boxed_str()),
}
}
fn line_number(index: usize) -> u32 {
u32::try_from(index.saturating_add(1)).unwrap_or(u32::MAX)
}