use std::fmt::Write as _;
use yog_data::Diagnostic;
#[derive(Debug, Clone, PartialEq)]
pub(super) enum Tok {
Ident(String),
Num(String),
Str(String),
PathSep,
Dot,
OptDot,
Open,
Close,
Comma,
Eq,
Ne,
Not,
}
const SYMBOLS: [(&str, Tok); 9] = [
("::", Tok::PathSep),
("?.", Tok::OptDot),
("==", Tok::Eq),
("!=", Tok::Ne),
("!", Tok::Not),
(".", Tok::Dot),
("(", Tok::Open),
(")", Tok::Close),
(",", Tok::Comma),
];
const QUOTE: char = '\'';
struct Lexer {
cs: Vec<char>,
i: usize,
}
impl Lexer {
fn take_while(&mut self, start: usize, more: impl Fn(&[char], usize) -> bool) -> String {
while self.i < self.cs.len() && more(&self.cs, self.i) {
self.i += 1;
}
self.cs[start..self.i].iter().collect()
}
fn number(&mut self) -> Tok {
let s = self.i;
self.i += 1;
Tok::Num(self.take_while(s, |cs, i| {
cs[i].is_ascii_alphanumeric()
|| cs[i] == '_'
|| (cs[i] == '.' && cs.get(i + 1).is_some_and(|n| n.is_ascii_digit()))
}))
}
fn string(&mut self) -> Result<Tok, Diagnostic> {
self.i += 1;
let text = self.take_while(self.i, |cs, i| cs[i] != QUOTE);
if self.i >= self.cs.len() {
return Err("unterminated 'string'".into());
}
self.i += 1;
Ok(Tok::Str(text))
}
fn symbol(&mut self) -> Option<Tok> {
let rest: String = self.cs[self.i..].iter().take(2).collect();
let (s, tok) = SYMBOLS.iter().find(|(s, _)| rest.starts_with(s))?;
self.i += s.chars().count();
Some(tok.clone())
}
fn next(&mut self) -> Result<Option<Tok>, Diagnostic> {
while self.cs.get(self.i).is_some_and(|c| c.is_whitespace()) {
self.i += 1;
}
let Some(&c) = self.cs.get(self.i) else {
return Ok(None);
};
let next = self.cs.get(self.i + 1).copied();
Ok(Some(match c {
_ if c.is_alphabetic() || c == '_' => Tok::Ident(self.take_while(self.i, |cs, i| cs[i].is_alphanumeric() || cs[i] == '_')),
_ if c.is_ascii_digit() || (c == '-' && next.is_some_and(|n| n.is_ascii_digit())) => self.number(),
QUOTE => self.string()?,
_ => self.symbol().ok_or_else(|| format!("unexpected `{c}` (bindings allow paths, calls, literals, !, == and !=; use a view-model method for anything else)"))?,
}))
}
}
pub(super) fn lex(src: &str) -> Result<Vec<Tok>, Diagnostic> {
let mut lexer = Lexer {
cs: src.chars().collect(),
i: 0,
};
let mut out = Vec::new();
while let Some(t) = lexer.next()? {
out.push(t);
}
Ok(out)
}
#[derive(Debug, Clone)]
pub(super) struct Seg {
pub(super) opt: bool,
pub(super) name: String,
pub(super) args: Option<Vec<Ex>>,
}
#[derive(Debug, Clone)]
pub(super) enum Head {
Ident(String, Option<Vec<Ex>>),
Type(String, Option<Vec<Ex>>),
Lit(String),
Paren(Box<Ex>),
}
#[derive(Debug, Clone)]
pub(super) enum Ex {
Post(Head, Vec<Seg>),
Not(Box<Ex>),
Cmp(Box<Ex>, &'static str, Box<Ex>),
}
pub(super) const VM: &str = "vm";
pub(super) const CX: &str = "cx";
pub(super) const ITEM: &str = "item";
pub(super) const INDEX: &str = "index";
const PATH_SEP: &str = "::";
struct ExParser {
toks: Vec<Tok>,
pos: usize,
}
impl ExParser {
fn peek(&self) -> Option<&Tok> {
self.toks.get(self.pos)
}
fn bump(&mut self) -> Option<Tok> {
let t = self.toks.get(self.pos).cloned();
self.pos += 1;
t
}
fn eat(&mut self, t: &Tok) -> bool {
let here = self.peek() == Some(t);
if here {
self.pos += 1;
}
here
}
fn expr(&mut self) -> Result<Ex, Diagnostic> {
let a = self.unary()?;
let op = if self.eat(&Tok::Eq) {
"=="
} else if self.eat(&Tok::Ne) {
"!="
} else {
return Ok(a);
};
let b = self.unary()?;
Ok(Ex::Cmp(Box::new(a), op, Box::new(b)))
}
fn unary(&mut self) -> Result<Ex, Diagnostic> {
if self.eat(&Tok::Not) {
return Ok(Ex::Not(Box::new(self.postfix()?)));
}
self.postfix()
}
fn call(&mut self) -> Result<Option<Vec<Ex>>, Diagnostic> {
if !self.eat(&Tok::Open) {
return Ok(None);
}
let mut args = Vec::new();
if self.eat(&Tok::Close) {
return Ok(Some(args));
}
loop {
args.push(self.expr()?);
if self.eat(&Tok::Close) {
return Ok(Some(args));
}
if !self.eat(&Tok::Comma) {
return Err("expected `,` or `)`".into());
}
}
}
fn named(&mut self, id: String) -> Result<Head, Diagnostic> {
if self.peek() != Some(&Tok::PathSep) {
if id == yog_ids::yui_value::TRUE || id == yog_ids::yui_value::FALSE {
return Ok(Head::Lit(id));
}
return Ok(Head::Ident(id, self.call()?));
}
let mut path = id;
while self.eat(&Tok::PathSep) {
let Some(Tok::Ident(n)) = self.bump() else {
return Err("expected a name after `::`".into());
};
path.push_str(PATH_SEP);
path.push_str(&n);
}
Ok(Head::Type(path, self.call()?))
}
fn head(&mut self) -> Result<Head, Diagnostic> {
match self.bump() {
Some(Tok::Ident(id)) => self.named(id),
Some(Tok::Num(n)) => Ok(Head::Lit(n)),
Some(Tok::Str(s)) => Ok(Head::Lit(format!("{s:?}"))),
Some(Tok::Open) => {
let e = self.expr()?;
if !self.eat(&Tok::Close) {
return Err("expected `)`".into());
}
Ok(Head::Paren(Box::new(e)))
}
Some(t) => Err(format!("unexpected {t:?}").into()),
None => Err("expected an expression".into()),
}
}
fn postfix(&mut self) -> Result<Ex, Diagnostic> {
let head = self.head()?;
let mut segs = Vec::new();
loop {
let opt = if self.eat(&Tok::Dot) {
false
} else if self.eat(&Tok::OptDot) {
true
} else {
break;
};
let Some(Tok::Ident(name)) = self.bump() else {
return Err("expected a name after `.`".into());
};
let args = self.call()?;
segs.push(Seg { opt, name, args });
}
Ok(Ex::Post(head, segs))
}
}
pub(super) fn parse_expr(src: &str) -> Result<Ex, Diagnostic> {
let mut p = ExParser {
toks: lex(src)?,
pos: 0,
};
let e = p.expr()?;
if let Some(t) = p.toks.get(p.pos) {
return Err(format!("unexpected {t:?}").into());
}
Ok(e)
}
#[derive(Clone, Copy, PartialEq)]
pub(super) enum Scope {
Screen,
Template,
}
impl Ex {
pub(super) fn has_opt(&self) -> bool {
match self {
Ex::Post(h, segs) => {
let head = match h {
Head::Ident(_, a) | Head::Type(_, a) => a.iter().flatten().any(Ex::has_opt),
Head::Paren(e) => e.has_opt(),
Head::Lit(_) => false,
};
head || segs
.iter()
.any(|s| s.opt || s.args.iter().flatten().any(Ex::has_opt))
}
Ex::Not(e) => e.has_opt(),
Ex::Cmp(a, _, b) => a.has_opt() || b.has_opt(),
}
}
pub(super) fn is_place(&self) -> bool {
match self {
Ex::Post(Head::Ident(n, None), segs) => {
n != CX && segs.iter().all(|s| !s.opt && s.args.is_none())
}
_ => false,
}
}
fn args(
a: &Option<Vec<Ex>>,
by_ref: bool,
scope: Scope,
command: bool,
) -> Result<String, Diagnostic> {
let Some(a) = a else { return Ok(String::new()) };
let parts = a
.iter()
.map(|e| {
let s = e.emit(scope, command)?;
Ok(if by_ref && e.is_place() {
format!("&{s}")
} else {
s
})
})
.collect::<Result<Vec<_>, Diagnostic>>()?;
Ok(format!("({})", parts.join(", ")))
}
fn root(n: &str, scope: Scope, command: bool) -> Result<String, Diagnostic> {
Ok(match (n, scope) {
(VM, _) => VM.to_string(),
(CX, _) if command => CX.to_string(),
(CX, _) => return Err("`cx` is only available in commands".into()),
(ITEM | INDEX, Scope::Template) => n.to_string(),
(ITEM | INDEX, Scope::Screen) => {
return Err(format!("`{n}` is only available in item templates").into());
}
(_, Scope::Screen) => format!("{VM}.{n}"),
(_, Scope::Template) => format!("{ITEM}.{n}"),
})
}
pub(super) fn emit(&self, scope: Scope, command: bool) -> Result<String, Diagnostic> {
Ok(match self {
Ex::Post(h, segs) => {
let mut s = match h {
Head::Lit(l) => l.clone(),
Head::Paren(e) => format!("({})", e.emit(scope, command)?),
Head::Type(p, a) => format!("{p}{}", Ex::args(a, true, scope, command)?),
Head::Ident(n, a) => format!(
"{}{}",
Ex::root(n, scope, command)?,
Ex::args(a, false, scope, command)?
),
};
for seg in segs {
if seg.opt {
s.push('?');
}
let _ = write!(
s,
".{}{}",
seg.name,
Ex::args(&seg.args, false, scope, command)?
);
}
s
}
Ex::Not(e) => format!("!{}", e.emit(scope, command)?),
Ex::Cmp(a, op, b) => format!(
"{} {op} {}",
a.emit(scope, command)?,
b.emit(scope, command)?
),
})
}
}