use std::sync::Arc;
use tabnas::{Context, Lexer, Rule, Tabnas, Token, Value, TIN_NR, TIN_TX, TIN_VL};
use crate::ast::MAX_NESTING;
pub(crate) const MATCHER: &str = "@alchemy-layout";
pub(crate) const IN: &str = "#IN";
pub(crate) const DE: &str = "#DE";
pub(crate) const NL: &str = "#NL";
pub(crate) const KW: &str = "#KW";
const K_STATE: &str = "alchemyLayout";
const S_SEEN: usize = 0;
const S_LEVELS: usize = 1;
const S_PENDING: usize = 2;
const S_DEPTH: usize = 3;
const S_DEPTH_POS: usize = 4;
const S_IN_STRING: usize = 5;
const S_IN_COMMENT: usize = 6;
const S_CHECKED: usize = 7;
const S_LEN: usize = 8;
pub(crate) fn register(parser: &mut Tabnas) {
for name in [IN, DE, NL, KW] {
parser.token(name);
}
parser.imperative_lex_match_ref(MATCHER, layout_matcher);
}
pub fn is_symbol_char(c: char) -> bool {
c.is_ascii_alphanumeric()
|| matches!(
c,
'_' | '-'
| '?'
| '!'
| '*'
| '+'
| '/'
| '<'
| '>'
| '='
| '.'
| '$'
| '%'
| '&'
| '|'
| '^'
| '~'
| '@'
)
}
pub fn is_json_number(text: &str) -> bool {
let b = text.as_bytes();
let mut i = 0;
if b.get(i) == Some(&b'-') {
i += 1;
}
match b.get(i) {
Some(b'0') => i += 1,
Some(c) if c.is_ascii_digit() => {
while b.get(i).is_some_and(u8::is_ascii_digit) {
i += 1;
}
}
_ => return false,
}
if b.get(i) == Some(&b'.') {
i += 1;
let start = i;
while b.get(i).is_some_and(u8::is_ascii_digit) {
i += 1;
}
if i == start {
return false;
}
}
if matches!(b.get(i), Some(b'e' | b'E')) {
i += 1;
if matches!(b.get(i), Some(b'+' | b'-')) {
i += 1;
}
let start = i;
while b.get(i).is_some_and(u8::is_ascii_digit) {
i += 1;
}
if i == start {
return false;
}
}
i == b.len()
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct LayoutState {
pub(crate) seen: bool,
pub(crate) levels: usize,
pub(crate) pending: usize,
pub(crate) depth: usize,
pub(crate) depth_pos: usize,
pub(crate) in_string: bool,
pub(crate) in_comment: bool,
pub(crate) checked: usize,
}
fn flag(fields: &[Value], index: usize) -> bool {
matches!(fields.get(index), Some(Value::Bool(true)))
}
fn count(fields: &[Value], index: usize) -> usize {
match fields.get(index) {
Some(Value::Number(n)) if n.is_finite() && *n >= 0.0 => *n as usize,
_ => 0,
}
}
impl LayoutState {
pub(crate) fn top(&self) -> usize {
2 * self.levels
}
pub(crate) fn load(context: &Context) -> Self {
let Some(Value::Array(fields)) = context.u.get(K_STATE) else {
return LayoutState::default();
};
LayoutState {
seen: flag(fields, S_SEEN),
levels: count(fields, S_LEVELS),
pending: count(fields, S_PENDING),
depth: count(fields, S_DEPTH),
depth_pos: count(fields, S_DEPTH_POS),
in_string: flag(fields, S_IN_STRING),
in_comment: flag(fields, S_IN_COMMENT),
checked: count(fields, S_CHECKED),
}
}
pub(crate) fn store(&self, context: &mut Context) {
let fields = [
Value::Bool(self.seen),
Value::Number(self.levels as f64),
Value::Number(self.pending as f64),
Value::Number(self.depth as f64),
Value::Number(self.depth_pos as f64),
Value::Bool(self.in_string),
Value::Bool(self.in_comment),
Value::Number(self.checked as f64),
];
if let Some(Value::Array(current)) = context.u.get_mut(K_STATE) {
if current.len() == S_LEN {
for (slot, field) in Arc::make_mut(current).iter_mut().zip(fields) {
*slot = field;
}
return;
}
}
context
.u
.insert(K_STATE.to_string(), Value::array(fields.into()));
}
pub(crate) fn depth_at(&mut self, src: &str, target: usize) -> usize {
let target = target.min(src.len());
if target < self.depth_pos {
self.depth = 0;
self.depth_pos = 0;
self.in_string = false;
self.in_comment = false;
}
let bytes = src.as_bytes();
let mut i = self.depth_pos;
while i < target {
let c = bytes[i];
if self.in_string {
match c {
b'\\' => i += 1,
b'"' | b'\n' | b'\r' => self.in_string = false,
_ => {}
}
} else if self.in_comment {
if c == b'\n' || c == b'\r' {
self.in_comment = false;
}
} else {
match c {
b'(' | b'[' => self.depth += 1,
b')' | b']' => self.depth = self.depth.saturating_sub(1),
b'"' => self.in_string = true,
b';' => self.in_comment = true,
_ => {}
}
}
i += 1;
}
self.depth_pos = target;
self.depth
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum Decision {
Pass,
Word {
name: &'static str,
tin: tabnas::Tin,
value: Value,
len: usize,
},
Layout {
name: &'static str,
len: usize,
pending: usize,
},
Bad {
code: &'static str,
at: usize,
row_cr: Option<usize>,
},
}
pub(crate) fn decide(src: &str, si: usize, state: &mut LayoutState) -> Decision {
if state.pending > 0 {
state.pending -= 1;
return Decision::Layout {
name: DE,
len: 0,
pending: state.pending,
};
}
let rest = &src[si..];
let Some(first) = rest.chars().next() else {
return Decision::Pass;
};
if si == 0 && !state.seen && (first == ' ' || first == '\t') {
return line_start(src, 0, false, state);
}
if !state.seen && si < state.checked && matches!(first, '\n' | '\r') {
return Decision::Pass;
}
match first {
'\n' => line_end(src, si, state),
'\r' if rest.as_bytes().get(1) == Some(&b'\n') => line_end(src, si, state),
'\r' if !state.seen => line_start(src, si, false, state),
' ' | '\t' | '\r' | ';' => Decision::Pass,
'(' | '[' => {
state.seen = true;
let nesting = 1 + state.levels + state.depth_at(src, si + 1);
if nesting > MAX_NESTING {
Decision::Bad {
code: "too_deep",
at: si,
row_cr: None,
}
} else {
Decision::Pass
}
}
')' | ']' => {
state.seen = true;
if state.depth_at(src, si) == 0 {
Decision::Bad {
code: "unbalanced",
at: si,
row_cr: None,
}
} else {
Decision::Pass
}
}
':' => {
state.seen = true;
let name_len = symbol_run(&rest[1..]);
if name_len == 0 {
return Decision::Pass;
}
Decision::Word {
name: KW,
tin: -1,
value: Value::String(rest[1..1 + name_len].to_string()),
len: 1 + name_len,
}
}
c if is_symbol_char(c) => {
state.seen = true;
let len = symbol_run(rest);
let word = &rest[..len];
if is_json_number(word) {
let value = word.parse::<f64>().unwrap_or(f64::NAN);
Decision::Word {
name: "#NR",
tin: TIN_NR,
value: Value::Number(value),
len,
}
} else {
match word {
"true" | "false" => Decision::Word {
name: "#VL",
tin: TIN_VL,
value: Value::Bool(word == "true"),
len,
},
"null" => Decision::Word {
name: "#VL",
tin: TIN_VL,
value: Value::Null,
len,
},
_ => Decision::Word {
name: "#TX",
tin: TIN_TX,
value: Value::String(word.to_string()),
len,
},
}
}
}
_ => {
state.seen = true;
Decision::Pass
}
}
}
fn line_end(src: &str, si: usize, state: &mut LayoutState) -> Decision {
if state.depth_at(src, si) > 0 {
Decision::Pass
} else {
line_start(src, si, true, state)
}
}
fn symbol_run(text: &str) -> usize {
text.find(|c: char| !is_symbol_char(c))
.unwrap_or(text.len())
}
fn after_terminator(bytes: &[u8], pos: usize) -> usize {
match (bytes.get(pos), bytes.get(pos + 1)) {
(Some(b'\n'), _) => pos + 1,
(Some(b'\r'), Some(b'\n')) => pos + 2,
_ => pos,
}
}
fn line_start(src: &str, from: usize, after_newline: bool, state: &mut LayoutState) -> Decision {
#[cfg(test)]
tests::SCANS.with(|scans| scans.set(scans.get() + 1));
let bytes = src.as_bytes();
let mut pos = if after_newline {
after_terminator(bytes, from)
} else {
from
};
loop {
let mut spaces = 0;
let mut tab_at = None;
let mut first_cr = None;
let mut last_cr = None;
loop {
while let Some(c) = bytes.get(pos) {
match c {
b' ' => spaces += 1,
b'\t' => {
tab_at.get_or_insert(pos);
}
_ => break,
}
pos += 1;
}
if bytes.get(pos) == Some(&b';') {
while bytes.get(pos).is_some_and(|c| *c != b'\n' && *c != b'\r') {
pos += 1;
}
}
if bytes.get(pos) == Some(&b'\r') && bytes.get(pos + 1) != Some(&b'\n') {
first_cr.get_or_insert(pos);
last_cr = Some(pos);
pos += 1;
spaces = 0;
tab_at = None;
continue;
}
break;
}
match bytes.get(pos) {
None => {
state.checked = state.checked.max(src.len());
return Decision::Pass;
}
Some(b'\n') => pos += 1,
Some(b'\r') if bytes.get(pos + 1) == Some(&b'\n') => pos += 2,
Some(_) => {
if let Some(at) = tab_at {
return Decision::Bad {
code: "tab_indent",
at,
row_cr: last_cr,
};
}
return layout_token(spaces, pos, from, first_cr, last_cr, state);
}
}
}
}
fn layout_token(
indent: usize,
content: usize,
from: usize,
first_cr: Option<usize>,
last_cr: Option<usize>,
state: &mut LayoutState,
) -> Decision {
let row_cr = last_cr;
if !state.seen {
return if indent == 0 {
state.checked = state.checked.max(content);
Decision::Pass
} else {
Decision::Bad {
code: "bad_indent",
at: content,
row_cr,
}
};
}
let len = first_cr.unwrap_or(content) - from;
let top = state.top();
if indent == top {
return Decision::Layout {
name: NL,
len,
pending: 0,
};
}
if indent == top + 2 {
if 1 + state.levels + 1 > MAX_NESTING {
return Decision::Bad {
code: "too_deep",
at: content,
row_cr,
};
}
state.levels += 1;
return Decision::Layout {
name: IN,
len,
pending: 0,
};
}
if indent > top {
return Decision::Bad {
code: "bad_indent",
at: content,
row_cr,
};
}
if indent % 2 != 0 {
return Decision::Bad {
code: "bad_dedent",
at: content,
row_cr,
};
}
let pops = (top - indent) / 2;
state.levels -= pops;
state.pending = pops - 1;
Decision::Layout {
name: DE,
len,
pending: state.pending,
}
}
fn layout_matcher(lexer: &mut Lexer<'_>, _rule: &mut Rule, context: &mut Context) -> Option<Token> {
let point = lexer.point();
let si = point.site.si;
let loaded = LayoutState::load(context);
let mut state = loaded;
let decision = decide(lexer.source(), si, &mut state);
if state != loaded {
state.store(context);
}
let advance = |lexer: &mut Lexer<'_>, len: usize| -> String {
let text = lexer.source()[si..si + len].to_string();
let chars = text.chars().count();
if chars > 0 {
lexer.advance_chars(chars);
}
text
};
match decision {
Decision::Pass => None,
Decision::Word {
name,
tin,
value,
len,
} => {
let text = advance(lexer, len);
Some(Token::new(name, tin, value, text, point))
}
Decision::Layout { name, len, .. } => {
let text = advance(lexer, len);
Some(Token::new(name, -1, Value::Undefined, text, point))
}
Decision::Bad { code, at, row_cr } => {
advance(lexer, at - si);
let mut token = lexer.bad(code);
if let Some(cr) = row_cr {
token.site.ci = lexer.source()[cr + 1..at].chars().count() + 1;
}
Some(token)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
thread_local! {
pub(super) static SCANS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
fn words(src: &str) -> Vec<(String, String)> {
let mut state = LayoutState::default();
let mut out = Vec::new();
let mut si = 0;
while si < src.len() {
match decide(src, si, &mut state) {
Decision::Pass => {
out.push(("_".to_string(), src[si..si + 1].to_string()));
si += 1;
}
Decision::Word { name, len, .. } => {
out.push((name.to_string(), src[si..si + len].to_string()));
si += len;
}
Decision::Layout { name, len, .. } => {
out.push((name.to_string(), src[si..si + len].to_string()));
si += len;
}
Decision::Bad { code, .. } => {
out.push(("BAD".to_string(), code.to_string()));
break;
}
}
}
out
}
fn names(src: &str) -> String {
words(src)
.into_iter()
.map(|(name, text)| match name.as_str() {
"_" => text,
"#IN" | "#DE" | "#NL" => format!(" {name} "),
"BAD" => format!(" BAD:{text} "),
_ => format!("{name}({text})"),
})
.collect::<Vec<_>>()
.join("")
}
fn staircase(levels: usize) -> String {
(0..=levels)
.map(|level| format!("{}x", " ".repeat(level)))
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn json_numbers_are_recognised_exactly() {
for ok in ["0", "-0", "12", "1.5", "-1.5e10", "2E-3", "0.0"] {
assert!(is_json_number(ok), "{ok} is a JSON number");
}
for bad in ["01", "1.", ".5", "+1", "1e", "-", "1_000", "0x1f", "1a"] {
assert!(!is_json_number(bad), "{bad} is not a JSON number");
}
}
#[test]
fn words_split_into_numbers_values_and_symbols() {
assert_eq!(
names("def a-b? -1 1.5e3 true null :key x"),
"#TX(def) #TX(a-b?) #NR(-1) #NR(1.5e3) #VL(true) #VL(null) #KW(:key) #TX(x)"
);
}
#[test]
fn indentation_becomes_structural_tokens() {
assert_eq!(
names("a\n b\n c\n d\ne"),
"#TX(a) #IN #TX(b) #IN #TX(c) #DE #TX(d) #DE #TX(e)"
);
}
#[test]
fn a_dedent_of_two_levels_is_issued_one_token_per_call() {
let mut state = LayoutState::default();
let src = "a\n b\n c\nd";
let at = src.find("c\n").unwrap() + 1;
for (si, _) in src.char_indices().take_while(|(si, _)| *si < at) {
let _ = decide(src, si, &mut state);
}
assert_eq!(state.levels, 2);
let first = decide(src, at, &mut state);
assert_eq!(
first,
Decision::Layout {
name: DE,
len: 1,
pending: 1
}
);
let second = decide(src, at + 1, &mut state);
assert_eq!(
second,
Decision::Layout {
name: DE,
len: 0,
pending: 0
}
);
assert_eq!(state.levels, 0);
assert!(matches!(
decide(src, at + 1, &mut state),
Decision::Word { name: "#TX", .. }
));
}
#[test]
fn blank_and_comment_lines_do_not_count() {
assert_eq!(
names("a\n\n ; note\n \n b\n\n"),
"#TX(a) #IN #TX(b)\n\n"
);
}
#[test]
fn a_line_ends_at_a_line_feed_and_a_lone_carriage_return_is_whitespace() {
assert_eq!(names("a\r\n b\r\nc"), "#TX(a) #IN #TX(b) #DE #TX(c)");
assert_eq!(names("a\r b\r c"), "#TX(a)\r #TX(b)\r #TX(c)");
assert_eq!(names("a \r b"), "#TX(a) \r #TX(b)");
assert_eq!(names("a\n\r b"), "#TX(a) #IN \r #TX(b)");
assert_eq!(names("a\n \r b"), "#TX(a) #IN \r #TX(b)");
assert_eq!(names("a\n b\n \rc"), "#TX(a) #IN #TX(b) #DE \r#TX(c)");
assert_eq!(
names("a\n b\n \r \r c"),
"#TX(a) #IN #TX(b) #NL \r \r #TX(c)"
);
assert_eq!(names("a\n\t\r b"), "#TX(a) #IN \r #TX(b)");
assert_eq!(names("\r a"), " BAD:bad_indent ");
assert_eq!(names("\ra"), "\r#TX(a)");
assert_eq!(names("a\r\n\r b"), "#TX(a) #IN \r #TX(b)");
}
#[test]
fn the_trivia_before_the_first_form_is_scanned_once() {
for sep in ["\r", "\n", "\r\n"] {
let src = format!("{}\na b", format!("{sep};comment").repeat(10_000));
SCANS.with(|scans| scans.set(0));
let value = crate::parse_value(&src).expect("parses");
assert_eq!(value.to_json().to_string().matches("\"a\"").count(), 1);
assert_eq!(SCANS.with(std::cell::Cell::get), 1, "{sep:?}");
}
let mut state = LayoutState::default();
let src = "\r;x\r;y\n \nab";
assert!(matches!(decide(src, 0, &mut state), Decision::Pass));
assert_eq!(state.checked, src.len() - 2);
assert!(matches!(decide(src, 3, &mut state), Decision::Pass));
assert!(matches!(decide(src, 6, &mut state), Decision::Pass));
assert_eq!(state.checked, src.len() - 2);
let mut state = LayoutState::default();
assert!(matches!(decide("\r;x\n;y", 0, &mut state), Decision::Pass));
assert_eq!(state.checked, 6);
SCANS.with(|scans| scans.set(0));
words("a\n b\n c (d\n e)\nf");
assert_eq!(SCANS.with(std::cell::Cell::get), 3);
}
#[test]
fn a_line_holding_only_trivia_after_a_lone_carriage_return_is_blank() {
assert_eq!(names("f x\n\r"), "#TX(f) #TX(x)\n\r");
assert_eq!(names("f x\n \r "), "#TX(f) #TX(x)\n \r ");
assert_eq!(names("a\n b\n \r;c\nd"), "#TX(a) #IN #TX(b) #DE #TX(d)");
assert_eq!(names("a\n b\n\r;x\n c"), "#TX(a) #IN #TX(b) #NL #TX(c)");
assert_eq!(names("a\n;x\r c"), "#TX(a) #IN \r #TX(c)");
assert_eq!(names("\r;x\n a"), " BAD:bad_indent ");
}
#[test]
fn an_error_after_a_lone_carriage_return_counts_its_column_from_the_last() {
let mut state = LayoutState::default();
assert!(matches!(
decide(" \r a", 0, &mut state),
Decision::Bad {
code: "bad_indent",
at: 5,
row_cr: Some(2)
}
));
let mut state = LayoutState::default();
assert!(matches!(
decide("\r \r a", 0, &mut state),
Decision::Bad {
code: "bad_indent",
at: 6,
row_cr: Some(2)
}
));
let mut state = LayoutState {
seen: true,
..LayoutState::default()
};
assert!(matches!(
decide("a\n\r \tb", 1, &mut state),
Decision::Bad {
code: "tab_indent",
at: 4,
row_cr: Some(2)
}
));
let mut state = LayoutState {
seen: true,
..LayoutState::default()
};
assert!(matches!(
decide("a\n \r b", 1, &mut state),
Decision::Bad {
code: "bad_indent",
at: 8,
row_cr: Some(4)
}
));
}
#[test]
fn layout_is_suspended_inside_delimiters() {
assert_eq!(
names("a (b\n c)\n d"),
"#TX(a) (#TX(b)\n #TX(c)) #IN #TX(d)"
);
assert_eq!(names("[1\n2]"), "[#NR(1)\n#NR(2)]");
}
#[test]
fn a_stray_closer_is_unbalanced() {
assert_eq!(names("a )"), "#TX(a) BAD:unbalanced ");
assert_eq!(names("(a))"), "(#TX(a)) BAD:unbalanced ");
}
#[test]
fn indentation_errors_have_their_own_codes() {
assert_eq!(names("a\n\tb"), "#TX(a) BAD:tab_indent ");
assert_eq!(names("a\n b"), "#TX(a) BAD:bad_indent ");
assert_eq!(names("a\n b\n c"), "#TX(a) #IN #TX(b) BAD:bad_dedent ");
assert_eq!(names(" a"), " BAD:bad_indent ");
assert_eq!(names("\n\n a"), " BAD:bad_indent ");
}
#[test]
fn nesting_past_the_bound_is_too_deep_at_the_opener_or_the_line() {
let at_limit = "(".repeat(MAX_NESTING - 1);
assert_eq!(names(&at_limit), at_limit);
assert_eq!(
names(&"(".repeat(MAX_NESTING)),
format!("{at_limit} BAD:too_deep ")
);
assert!(!names(&staircase(MAX_NESTING - 1)).contains("BAD"));
assert!(names(&staircase(MAX_NESTING)).ends_with(" BAD:too_deep "));
let mixed = format!("{}\n{}[", staircase(2), " ".repeat(3));
assert!(names(&format!("{mixed}{}", "(".repeat(MAX_NESTING - 5))).ends_with('('));
assert!(
names(&format!("{mixed}{}", "(".repeat(MAX_NESTING - 4))).ends_with(" BAD:too_deep ")
);
}
#[test]
fn leading_trivia_before_the_first_line_issues_nothing() {
assert_eq!(names("\n\n \na"), "\n\n \n#TX(a)");
}
#[test]
fn the_depth_scan_ignores_strings_and_comments() {
let mut state = LayoutState::default();
let src = "(\"a)\" ; )\n)";
assert_eq!(state.depth_at(src, src.len() - 1), 1);
assert_eq!(state.depth_at(src, src.len()), 0);
assert_eq!(state.depth_at(src, 1), 1);
}
#[test]
fn state_round_trips_through_the_context_bag() {
use std::sync::Mutex;
let seen: Arc<Mutex<Vec<LayoutState>>> = Arc::new(Mutex::new(Vec::new()));
let mut parser = crate::make();
let record = Arc::clone(&seen);
parser.subscribe_lex(move |_token, _rule, context| {
if let Ok(mut states) = record.lock() {
states.push(LayoutState::load(context));
}
});
parser.parse("a\n b\n c\nd").expect("parses");
let states = seen.lock().expect("no panic held the lock");
assert!(
states.iter().any(|state| state.levels == 2 && state.seen),
"some token was lexed three levels deep: {states:?}"
);
let last = states.last().expect("tokens were lexed");
assert_eq!(
(last.seen, last.levels, last.pending, last.depth),
(true, 0, 0, 0)
);
}
#[test]
fn the_store_writes_in_place_and_only_what_changed() {
use std::sync::Mutex;
let identities: Arc<Mutex<Vec<(usize, usize)>>> = Arc::new(Mutex::new(Vec::new()));
let mut parser = crate::make();
let record = Arc::clone(&identities);
parser.subscribe_lex(move |_token, _rule, context| {
if let (Ok(mut seen), Some(Value::Array(fields))) =
(record.lock(), context.u.get(K_STATE))
{
seen.push((Arc::as_ptr(fields) as usize, context.u.len()));
}
});
parser.parse("a b\n c d\n(e\n f)\nx").expect("parses");
let seen = identities.lock().expect("no panic held the lock");
assert!(seen.len() > 4, "several tokens were lexed: {seen:?}");
assert!(
seen.windows(2).all(|pair| pair[0] == pair[1]),
"one array, one key, throughout: {seen:?}"
);
}
}