This commit is contained in:
2025-09-09 21:21:39 +10:00
parent 3cbeb5a2ad
commit c88d1965c3
11 changed files with 333 additions and 128 deletions

View File

@@ -2,7 +2,10 @@ use std::collections::HashSet;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::evaluator::*; use crate::{
common::{LeadErr, LeadErrCode},
evaluator::*,
};
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)] #[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub struct CellRef { pub struct CellRef {
@@ -93,7 +96,7 @@ impl Cell {
impl CellRef { impl CellRef {
// Zero indexed // Zero indexed
pub fn new(s: String) -> Result<CellRef, String> { pub fn new(s: String) -> Result<CellRef, LeadErr> {
let s = s.trim(); let s = s.trim();
let mut col: usize = 0; let mut col: usize = 0;
let mut i = 0; let mut i = 0;
@@ -111,20 +114,26 @@ impl CellRef {
} }
if col <= 0 { if col <= 0 {
return Err(format!( return Err(LeadErr {
"Parse error: missing column letters in cell ref: {s}" title: "Parse error.".into(),
)); desc: format!("Missing column letters in cell ref: {s}."),
code: LeadErrCode::Syntax,
});
} }
let row_part = &s[i..]; let row_part = &s[i..];
if row_part.is_empty() { if row_part.is_empty() {
return Err(format!( return Err(LeadErr {
"Parse error: missing column letters in cell ref: {s}" title: "Parse error.".into(),
)); desc: format!("Missing column letters in cell ref: {s}."),
code: LeadErrCode::Syntax,
});
} else if !row_part.chars().all(|c| c.is_ascii_digit()) { } else if !row_part.chars().all(|c| c.is_ascii_digit()) {
return Err(format!( return Err(LeadErr {
"Parse error: row part must be numeric in cell ref: {s}" title: "Parse error.".into(),
)); desc: format!("Row part must be numeric in cell ref: {s}."),
code: LeadErrCode::Syntax,
});
} }
if let Ok(row) = row_part.parse::<usize>() { if let Ok(row) = row_part.parse::<usize>() {
@@ -133,7 +142,11 @@ impl CellRef {
col: col - 1, col: col - 1,
}) })
} else { } else {
Err(format!("Parse error: invalid row number.")) Err(LeadErr {
title: "Parse error.".into(),
desc: format!("Invalid row number."),
code: LeadErrCode::Syntax,
})
} }
} }
} }

View File

@@ -0,0 +1,30 @@
use serde::{Deserialize, Serialize};
// Example usage:
//
// title: "Evaluation error."
// desc: "Function ADD requires numeric type arguments."
// code: TypeErr
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
pub struct LeadErr {
pub title: String,
pub desc: String,
pub code: LeadErrCode,
}
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
pub enum LeadErrCode {
DivZero,
TypeErr,
Syntax,
Server,
Unsupported,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", content = "value")]
pub enum Literal {
Number(f64),
Boolean(bool),
String(String),
}

View File

@@ -1,9 +1,11 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::cell::CellRef; use crate::cell::CellRef;
use crate::common::LeadErr;
use crate::common::LeadErrCode;
use crate::common::Literal;
use crate::grid::Grid; use crate::grid::Grid;
use crate::parser::*; use crate::parser::*;
use crate::tokenizer::Literal;
use std::collections::HashSet; use std::collections::HashSet;
use std::fmt; use std::fmt;
@@ -13,6 +15,7 @@ pub enum Eval {
Literal(Literal), Literal(Literal),
CellRef { eval: Box<Eval>, reference: CellRef }, CellRef { eval: Box<Eval>, reference: CellRef },
Range(Vec<Eval>), Range(Vec<Eval>),
Err(LeadErr),
Unset, Unset,
} }
@@ -23,19 +26,22 @@ impl fmt::Display for Eval {
Eval::Range(it) => write!(f, "Range({it:?})"), Eval::Range(it) => write!(f, "Range({it:?})"),
Eval::CellRef { eval, reference } => write!(f, "EvalRef({eval:?}, {reference:?})"), Eval::CellRef { eval, reference } => write!(f, "EvalRef({eval:?}, {reference:?})"),
Eval::Unset => write!(f, "Unset"), Eval::Unset => write!(f, "Unset"),
Eval::Err(it) => write!(f, "{it:?}"),
} }
} }
} }
pub fn evaluate(str: String, grid: Option<&Grid>) -> Result<(Eval, HashSet<CellRef>), String> { pub fn evaluate(str: String, grid: Option<&Grid>) -> (Eval, HashSet<CellRef>) {
let (expr, _) = parse(&str)?; match parse(&str) {
Ok((expr, _)) => {
let mut precs = HashSet::new(); let mut precs = HashSet::new();
// Make evaulator adds precs for ranges
match evaluate_expr(&expr, &mut precs, grid) { match evaluate_expr(&expr, &mut precs, grid) {
Ok(it) => Ok((it, precs)), Ok(it) => (it, precs),
Err(it) => Err(it), Err(it) => (Eval::Err(it), precs),
}
}
Err(e) => (Eval::Err(e), HashSet::new()),
} }
} }
@@ -43,7 +49,7 @@ fn evaluate_expr(
expr: &Expr, expr: &Expr,
precs: &mut HashSet<CellRef>, precs: &mut HashSet<CellRef>,
grid: Option<&Grid>, grid: Option<&Grid>,
) -> Result<Eval, String> { ) -> Result<Eval, LeadErr> {
let res = match expr { let res = match expr {
Expr::Literal(lit) => Eval::Literal(lit.clone()), Expr::Literal(lit) => Eval::Literal(lit.clone()),
Expr::CellRef(re) => { Expr::CellRef(re) => {
@@ -59,7 +65,11 @@ fn evaluate_expr(
}, },
} }
} else { } else {
return Err("Evaluation error: Found cell reference but no grid.".into()); return Err(LeadErr {
title: "Evaluation error.".into(),
desc: "Found cell reference with no grid.".into(),
code: LeadErrCode::Server,
});
} }
} }
Expr::Infix { op, lhs, rhs } => { Expr::Infix { op, lhs, rhs } => {
@@ -81,7 +91,13 @@ fn evaluate_expr(
InfixOp::MUL => eval_mul(&lval, &rval)?, InfixOp::MUL => eval_mul(&lval, &rval)?,
InfixOp::DIV => eval_div(&lval, &rval)?, InfixOp::DIV => eval_div(&lval, &rval)?,
InfixOp::RANGE => eval_range(&lval, &rval, precs, grid)?, InfixOp::RANGE => eval_range(&lval, &rval, precs, grid)?,
_ => return Err(format!("Evaluation error: Unsupported operator {:?}", op)), _ => {
return Err(LeadErr {
title: "Evaluation error.".into(),
desc: format!("Unsupported operator {:?}", op),
code: LeadErrCode::Unsupported,
});
}
} }
} }
Expr::Prefix { op, expr } => { Expr::Prefix { op, expr } => {
@@ -101,9 +117,21 @@ fn evaluate_expr(
Expr::Group(g) => evaluate_expr(g, precs, grid)?, Expr::Group(g) => evaluate_expr(g, precs, grid)?,
Expr::Function { name, args } => match name.as_str() { Expr::Function { name, args } => match name.as_str() {
"AVG" => eval_avg(args, precs, grid)?, "AVG" => eval_avg(args, precs, grid)?,
it => return Err(format!("Evaluation error: Unsupported function {}.", it)), it => {
return Err(LeadErr {
title: "Evaluation error.".into(),
desc: format!("Unsupported function {:?}", it),
code: LeadErrCode::Unsupported,
});
}
}, },
it => return Err(format!("Evaluation error: Unsupported expression {:?}", it)), it => {
return Err(LeadErr {
title: "Evaluation error.".into(),
desc: format!("Unsupported expression {:?}", it),
code: LeadErrCode::Unsupported,
});
}
}; };
Ok(res) Ok(res)
@@ -114,7 +142,7 @@ fn eval_range(
rval: &Eval, rval: &Eval,
precs: &mut HashSet<CellRef>, precs: &mut HashSet<CellRef>,
grid: Option<&Grid>, grid: Option<&Grid>,
) -> Result<Eval, String> { ) -> Result<Eval, LeadErr> {
match (lval, rval) { match (lval, rval) {
( (
Eval::CellRef { Eval::CellRef {
@@ -139,7 +167,11 @@ fn eval_range(
let reference = CellRef { row, col }; let reference = CellRef { row, col };
let Some(g) = grid else { let Some(g) = grid else {
return Err("Evaluation error: Found cell range but no grid.".into()); return Err(LeadErr {
title: "Evaluation error.".into(),
desc: "Found cell range but no grid.".into(),
code: LeadErrCode::Server,
});
}; };
cells.push(Eval::CellRef { cells.push(Eval::CellRef {
@@ -157,7 +189,11 @@ fn eval_range(
Ok(Eval::Range(cells)) Ok(Eval::Range(cells))
} }
_ => Err("Evaluation error: expected cellref types for RANGE function.".to_string()), _ => Err(LeadErr {
title: "Evaluation error.".into(),
desc: "Expected cell reference types for RANGE function.".into(),
code: LeadErrCode::Unsupported,
}),
} }
} }
@@ -165,7 +201,7 @@ fn eval_avg(
args: &Vec<Expr>, args: &Vec<Expr>,
precs: &mut HashSet<CellRef>, precs: &mut HashSet<CellRef>,
grid: Option<&Grid>, grid: Option<&Grid>,
) -> Result<Eval, String> { ) -> Result<Eval, LeadErr> {
let mut res = 0.0; let mut res = 0.0;
let mut count = 0; let mut count = 0;
@@ -178,7 +214,11 @@ fn eval_avg(
Eval::Range(range) => { Eval::Range(range) => {
for cell in range { for cell in range {
let Eval::CellRef { eval, reference: _ } = cell else { let Eval::CellRef { eval, reference: _ } = cell else {
panic!("Found non cellref in evaluation time RANGE!."); return Err(LeadErr {
title: "Evaluation error.".into(),
desc: "Found non-cellref in RANGE during AVG evaluation.".into(),
code: LeadErrCode::Server,
});
}; };
if let Eval::Literal(Literal::Number(num)) = *eval { if let Eval::Literal(Literal::Number(num)) = *eval {
@@ -187,27 +227,36 @@ fn eval_avg(
} else if matches!(*eval, Eval::Unset) { } else if matches!(*eval, Eval::Unset) {
continue; continue;
} else { } else {
return Err("Evaluation error: expected numeric types for AVG function." return Err(LeadErr {
.to_string()); title: "Evaluation error.".into(),
desc: "Expected numeric types for AVG function.".into(),
code: LeadErrCode::Unsupported,
});
} }
} }
} }
_ => { _ => {
return Err( return Err(LeadErr {
"Evaluation error: expected numeric types for AVG function.".to_string() title: "Evaluation error.".into(),
); desc: "Expected numeric types for AVG function.".into(),
code: LeadErrCode::Unsupported,
});
} }
} }
} }
if count == 0 { if count == 0 {
Err("Evaluation error: attempted to divide by zero.".to_string()) Err(LeadErr {
title: "Evaluation error.".into(),
desc: "Attempted to divide by zero.".into(),
code: LeadErrCode::DivZero,
})
} else { } else {
Ok(Eval::Literal(Literal::Number(res / count as f64))) Ok(Eval::Literal(Literal::Number(res / count as f64)))
} }
} }
fn eval_add(lval: &Eval, rval: &Eval) -> Result<Eval, String> { fn eval_add(lval: &Eval, rval: &Eval) -> Result<Eval, LeadErr> {
match (lval, rval) { match (lval, rval) {
(Eval::Literal(a), Eval::Literal(b)) => { (Eval::Literal(a), Eval::Literal(b)) => {
if let Some(res) = eval_numeric_infix(a, b, |x, y| x + y) { if let Some(res) = eval_numeric_infix(a, b, |x, y| x + y) {
@@ -221,46 +270,72 @@ fn eval_add(lval: &Eval, rval: &Eval) -> Result<Eval, String> {
return Ok(Eval::Literal(Literal::String(res))); return Ok(Eval::Literal(Literal::String(res)));
} }
Err("Evaluation error: expected string or numeric types for ADD function.".to_string()) Err(LeadErr {
} title: "Evaluation error.".into(),
_ => { desc: "Expected string or numeric types for ADD function.".into(),
Err("Evaluation error: expected string or numeric types for ADD function.".to_string()) code: LeadErrCode::Unsupported,
})
} }
_ => Err(LeadErr {
title: "Evaluation error.".into(),
desc: "Expected string or numeric types for ADD function.".into(),
code: LeadErrCode::Unsupported,
}),
} }
} }
fn eval_sub(lval: &Eval, rval: &Eval) -> Result<Eval, String> { fn eval_sub(lval: &Eval, rval: &Eval) -> Result<Eval, LeadErr> {
match (lval, rval) { match (lval, rval) {
(Eval::Literal(a), Eval::Literal(b)) => { (Eval::Literal(a), Eval::Literal(b)) => {
if let Some(res) = eval_numeric_infix(a, b, |x, y| x - y) { if let Some(res) = eval_numeric_infix(a, b, |x, y| x - y) {
return Ok(Eval::Literal(res)); return Ok(Eval::Literal(res));
} }
Err("Evaluation error: expected numeric types for SUB function.".to_string()) Err(LeadErr {
title: "Evaluation error.".into(),
desc: "Expected numeric types for SUB function.".into(),
code: LeadErrCode::Unsupported,
})
} }
_ => Err("Evaluation error: expected numeric types for SUB function.".to_string()), _ => Err(LeadErr {
title: "Evaluation error.".into(),
desc: "Expected numeric types for SUB function.".into(),
code: LeadErrCode::Unsupported,
}),
} }
} }
fn eval_mul(lval: &Eval, rval: &Eval) -> Result<Eval, String> {
fn eval_mul(lval: &Eval, rval: &Eval) -> Result<Eval, LeadErr> {
match (lval, rval) { match (lval, rval) {
(Eval::Literal(a), Eval::Literal(b)) => { (Eval::Literal(a), Eval::Literal(b)) => {
if let Some(res) = eval_numeric_infix(a, b, |x, y| x * y) { if let Some(res) = eval_numeric_infix(a, b, |x, y| x * y) {
return Ok(Eval::Literal(res)); return Ok(Eval::Literal(res));
} }
Err("Evaluation error: expected numeric types for MUL function.".to_string()) Err(LeadErr {
title: "Evaluation error.".into(),
desc: "Expected numeric types for MUL function.".into(),
code: LeadErrCode::Unsupported,
})
} }
_ => Err("Evaluation error: expected numeric types for MUL function.".to_string()), _ => Err(LeadErr {
title: "Evaluation error.".into(),
desc: "Expected numeric types for MUL function.".into(),
code: LeadErrCode::Unsupported,
}),
} }
} }
fn eval_div(lval: &Eval, rval: &Eval) -> Result<Eval, String> {
fn eval_div(lval: &Eval, rval: &Eval) -> Result<Eval, LeadErr> {
match (lval, rval) { match (lval, rval) {
(Eval::Literal(a), Eval::Literal(b)) => { (Eval::Literal(a), Eval::Literal(b)) => {
if let (Literal::Number(_), Literal::Number(y)) = (a, b) { if let (Literal::Number(_), Literal::Number(y)) = (a, b) {
if *y == 0f64 { if *y == 0f64 {
return Err( return Err(LeadErr {
"Evaluation error: integers attempted to divide by zero.".to_string() title: "Evaluation error.".into(),
); desc: "Attempted to divide by zero.".into(),
code: LeadErrCode::DivZero,
});
} }
} }
@@ -268,9 +343,50 @@ fn eval_div(lval: &Eval, rval: &Eval) -> Result<Eval, String> {
return Ok(Eval::Literal(res)); return Ok(Eval::Literal(res));
} }
Err("Evaluation error: expected numeric types for DIV function.".to_string()) Err(LeadErr {
title: "Evaluation error.".into(),
desc: "Expected numeric types for DIV function.".into(),
code: LeadErrCode::Unsupported,
})
} }
_ => Err("Evaluation error: expected numeric types for DIV function.".to_string()), _ => Err(LeadErr {
title: "Evaluation error.".into(),
desc: "Expected numeric types for DIV function.".into(),
code: LeadErrCode::Unsupported,
}),
}
}
fn eval_pos(val: &Eval) -> Result<Eval, LeadErr> {
match val {
Eval::Literal(Literal::Number(it)) => Ok(Eval::Literal(Literal::Number(*it))),
_ => Err(LeadErr {
title: "Evaluation error.".into(),
desc: "Expected numeric type for POS function.".into(),
code: LeadErrCode::Unsupported,
}),
}
}
fn eval_neg(val: &Eval) -> Result<Eval, LeadErr> {
match val {
Eval::Literal(Literal::Number(it)) => Ok(Eval::Literal(Literal::Number(-it))),
_ => Err(LeadErr {
title: "Evaluation error.".into(),
desc: "Expected numeric type for NEG function.".into(),
code: LeadErrCode::Unsupported,
}),
}
}
fn eval_not(val: &Eval) -> Result<Eval, LeadErr> {
match val {
Eval::Literal(Literal::Boolean(it)) => Ok(Eval::Literal(Literal::Boolean(!it))),
_ => Err(LeadErr {
title: "Evaluation error.".into(),
desc: "Expected boolean type for NOT function.".into(),
code: LeadErrCode::Unsupported,
}),
} }
} }
@@ -280,23 +396,3 @@ fn eval_numeric_infix(lhs: &Literal, rhs: &Literal, op: fn(f64, f64) -> f64) ->
_ => None, _ => None,
} }
} }
fn eval_pos(val: &Eval) -> Result<Eval, String> {
match val {
Eval::Literal(Literal::Number(it)) => Ok(Eval::Literal(Literal::Number(*it))),
_ => Err("Evaluation error: expected numeric type for POS function.".to_string()),
}
}
fn eval_neg(val: &Eval) -> Result<Eval, String> {
match val {
Eval::Literal(Literal::Number(it)) => Ok(Eval::Literal(Literal::Number(-it))),
_ => Err("Evaluation error: expected numeric type for NEG function.".to_string()),
}
}
fn eval_not(val: &Eval) -> Result<Eval, String> {
match val {
Eval::Literal(Literal::Boolean(it)) => Ok(Eval::Literal(Literal::Boolean(!it))),
_ => Err("Evaluation error: expected boolean type for NEG function.".to_string()),
}
}

View File

@@ -4,8 +4,8 @@ use log::info;
use crate::{ use crate::{
cell::{Cell, CellRef}, cell::{Cell, CellRef},
common::Literal,
evaluator::{Eval, evaluate}, evaluator::{Eval, evaluate},
tokenizer::Literal,
}; };
pub struct Grid { pub struct Grid {
@@ -36,7 +36,7 @@ impl Grid {
eval = Eval::Literal(Literal::String(raw_val.to_owned())); eval = Eval::Literal(Literal::String(raw_val.to_owned()));
} else { } else {
// Evaluate raw expr and get precedents // Evaluate raw expr and get precedents
let (res_eval, res_precs) = evaluate(raw_val[1..].to_owned(), Some(&self))?; let (res_eval, res_precs) = evaluate(raw_val[1..].to_owned(), Some(&self));
eval = res_eval; eval = res_eval;
precs = res_precs; precs = res_precs;
} }
@@ -88,7 +88,7 @@ impl Grid {
search_set.extend(cell_data.deps().iter()); search_set.extend(cell_data.deps().iter());
temp.insert(from); temp.insert(from);
perm.insert(from); perm.insert(from); // Make this inside the inner topo_visit
let mut searched = 1; let mut searched = 1;
@@ -249,7 +249,7 @@ impl Grid {
}; };
// Now we dropped the borrow of self.cells before this point // Now we dropped the borrow of self.cells before this point
let (e, _) = evaluate(raw, Some(self))?; let (e, _) = evaluate(raw, Some(self));
if let Some(cell) = self.cells.get_mut(&cell_ref) { if let Some(cell) = self.cells.get_mut(&cell_ref) {
cell.set_eval(e); cell.set_eval(e);

View File

@@ -1,4 +1,5 @@
mod cell; mod cell;
mod common;
mod evaluator; mod evaluator;
mod grid; mod grid;
mod messages; mod messages;

View File

@@ -1,6 +1,10 @@
use log::info; use log::info;
use crate::{cell::CellRef, tokenizer::*}; use crate::{
cell::CellRef,
common::{LeadErr, LeadErrCode, Literal},
tokenizer::*,
};
use std::{collections::HashSet, fmt}; use std::{collections::HashSet, fmt};
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
@@ -159,7 +163,7 @@ impl Expr {
} }
} }
pub fn parse(input: &str) -> Result<(Expr, HashSet<CellRef>), String> { pub fn parse(input: &str) -> Result<(Expr, HashSet<CellRef>), LeadErr> {
let mut tokenizer = Tokenizer::new(input)?; let mut tokenizer = Tokenizer::new(input)?;
let mut precs = HashSet::new(); let mut precs = HashSet::new();
let expr = _parse(&mut tokenizer, 0, &mut precs)?; let expr = _parse(&mut tokenizer, 0, &mut precs)?;
@@ -171,13 +175,17 @@ pub fn _parse(
input: &mut Tokenizer, input: &mut Tokenizer,
min_prec: u8, min_prec: u8,
precedents: &mut HashSet<CellRef>, precedents: &mut HashSet<CellRef>,
) -> Result<Expr, String> { ) -> Result<Expr, LeadErr> {
let mut lhs = match input.next() { let mut lhs = match input.next() {
Token::Literal(it) => Expr::Literal(it), Token::Literal(it) => Expr::Literal(it),
Token::OpenParen => { Token::OpenParen => {
let lhs = _parse(input, 0, precedents)?; let lhs = _parse(input, 0, precedents)?;
if input.next() != Token::CloseParen { if input.next() != Token::CloseParen {
return Err(format!("Parse error: expected closing paren.")); return Err(LeadErr {
title: "Parse error.".into(),
desc: "Expected closing paren.".into(),
code: LeadErrCode::Syntax,
});
} }
Expr::Group(Box::new(lhs)) Expr::Group(Box::new(lhs))
} }
@@ -186,7 +194,13 @@ pub fn _parse(
'+' => PrefixOp::POS, '+' => PrefixOp::POS,
'-' => PrefixOp::NEG, '-' => PrefixOp::NEG,
'!' => PrefixOp::NOT, '!' => PrefixOp::NOT,
it => return Err(format!("Parse error: unknown prefix operator {:?}.", it)), it => {
return Err(LeadErr {
title: "Parse error.".into(),
desc: format!("Unknown prefix operator {:?}.", it),
code: LeadErrCode::Syntax,
});
}
}; };
let rhs = _parse(input, prefix_op.prec().1, precedents)?; let rhs = _parse(input, prefix_op.prec().1, precedents)?;
@@ -208,10 +222,13 @@ pub fn _parse(
input.next(); input.next();
break; break;
} else if nxt != Token::Comma && args.len() != 0 { } else if nxt != Token::Comma && args.len() != 0 {
return Err(format!( return Err(LeadErr {
"Parse error: expected comma while parsing argument of function {:?}.", title: "Parse error.".into(),
id desc: format!(
)); "Expected comma while parsing argument of function {id:?}"
),
code: LeadErrCode::Syntax,
});
} }
if args.len() != 0 { if args.len() != 0 {
@@ -234,7 +251,13 @@ pub fn _parse(
} }
}, },
it => return Err(format!("Parse error: did not expect token {:?}.", it)), it => {
return Err(LeadErr {
title: "Parse error.".into(),
desc: format!("Unexpected token {:?}.", it),
code: LeadErrCode::Syntax,
});
}
}; };
// In the reference article this is a loop with match // In the reference article this is a loop with match
@@ -250,7 +273,11 @@ pub fn _parse(
'|' => InfixOp::OR, '|' => InfixOp::OR,
':' => InfixOp::RANGE, ':' => InfixOp::RANGE,
it => { it => {
return Err(format!("Parse error: do not know infix operator {:?}.", it)); return Err(LeadErr {
title: "Parse error.".into(),
desc: format!("Unknown infix operator {:?}.", it),
code: LeadErrCode::Syntax,
});
} }
}; };
@@ -270,10 +297,11 @@ pub fn _parse(
let postfix_op = match op { let postfix_op = match op {
'%' => PostfixOp::PERCENT, '%' => PostfixOp::PERCENT,
it => { it => {
return Err(format!( return Err(LeadErr {
"Parse error: do not know postfix operator {:?}.", title: "Parse error.".into(),
it desc: format!("Unknown postfix operator {:?}.", it),
)); code: LeadErrCode::Syntax,
});
} }
}; };

View File

@@ -1,18 +1,11 @@
use serde::{Deserialize, Serialize}; use crate::common::{LeadErr, LeadErrCode, Literal};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", content = "value")]
pub enum Literal {
Number(f64),
Boolean(bool),
String(String),
}
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum Token { pub enum Token {
Identifier(String), // Could be a function Identifier(String), // Could be a function
Literal(Literal), Literal(Literal),
Operator(char), Operator(char),
Err(LeadErr),
OpenParen, OpenParen,
CloseParen, CloseParen,
Comma, Comma,
@@ -26,7 +19,7 @@ pub struct Tokenizer {
} }
impl Tokenizer { impl Tokenizer {
pub fn new(input: &str) -> Result<Tokenizer, String> { pub fn new(input: &str) -> Result<Tokenizer, LeadErr> {
let mut tokens = Vec::new(); let mut tokens = Vec::new();
let mut chars = input.chars().peekable(); let mut chars = input.chars().peekable();
@@ -113,7 +106,11 @@ impl Tokenizer {
tokens.push(Token::Comma); tokens.push(Token::Comma);
chars.next(); chars.next();
} else { } else {
return Err(format!("Encountered unknown token char: {c}")); return Err(LeadErr {
title: "Tokenizer error.".into(),
desc: format!("Encountered unknown token char: {c}"),
code: LeadErrCode::Syntax,
});
} }
} }

View File

@@ -1,13 +1,13 @@
<script lang="ts"> <script lang="ts">
import { Input } from '$lib/components/ui/input/index.js'; import { Input } from '$lib/components/ui/input/index.js';
import clsx from 'clsx'; import clsx from 'clsx';
import type { CellT } from './utils';
let { let {
cla = '', cla = '',
width = '80px', width = '80px',
height = '30px', height = '30px',
raw_val = $bindable(''), cell = $bindable(undefined),
val = undefined,
onmousedown = () => {}, onmousedown = () => {},
startediting = () => {}, startediting = () => {},
stopediting = () => {}, stopediting = () => {},
@@ -17,8 +17,7 @@
cla?: string; cla?: string;
width?: string; width?: string;
height?: string; height?: string;
raw_val?: string; cell?: CellT;
val?: LiteralValue | undefined;
onmousedown?: (e: MouseEvent) => void; onmousedown?: (e: MouseEvent) => void;
startediting?: () => void; startediting?: () => void;
stopediting?: () => void; stopediting?: () => void;
@@ -31,7 +30,7 @@
queueMicrotask(() => { queueMicrotask(() => {
const el = node.querySelector('input') as HTMLInputElement | null; const el = node.querySelector('input') as HTMLInputElement | null;
if (el !== null) { if (el !== null) {
el.value = raw_val; el.value = cell?.raw_val ?? '';
el.focus(); el.focus();
} }
}); });
@@ -56,7 +55,11 @@
class="relative rounded-none p-1 !transition-none delay-0 duration-0 class="relative rounded-none p-1 !transition-none delay-0 duration-0
focus:z-20 focus:shadow-[0_0_0_1px_var(--color-primary)] focus:outline-none" focus:z-20 focus:shadow-[0_0_0_1px_var(--color-primary)] focus:outline-none"
onblur={(e) => { onblur={(e) => {
raw_val = (e.target as HTMLInputElement).value; cell = {
isErr: false,
val: undefined,
raw_val: (e.target as HTMLInputElement).value
};
stopediting(); stopediting();
}} }}
/> />
@@ -69,12 +72,12 @@
style:height style:height
class={clsx('placeholder bg-background p-1', { active }, cla)} class={clsx('placeholder bg-background p-1', { active }, cla)}
> >
{#if raw_val !== '' || val !== ''} {#if cell && (cell.raw_val !== '' || cell.val !== '')}
<span class="pointer-events-none select-none"> <span class={clsx('pointer-events-none select-none', { err: cell.isErr })}>
{#if val !== undefined} {#if cell.val}
{val} {cell.val}
{:else} {:else}
{raw_val} {cell.raw_val}
{/if} {/if}
</span> </span>
{/if} {/if}
@@ -94,4 +97,21 @@
border: 1px solid var(--color-primary); border: 1px solid var(--color-primary);
outline: 1px solid var(--color-primary); outline: 1px solid var(--color-primary);
} }
.active:has(.err),
.placeholder:has(.err) {
position: relative; /* needed for absolute positioning */
}
.active:has(.err)::after,
.placeholder:has(.err)::after {
content: '';
position: absolute;
top: 0;
right: 0;
width: 0;
height: 0;
border-top: 12px solid red; /* size & color of the triangle */
border-left: 12px solid transparent;
}
</style> </style>

View File

@@ -3,7 +3,7 @@
import Cell from '$lib/components/grid/cell.svelte'; import Cell from '$lib/components/grid/cell.svelte';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import CellHeader from './cell-header.svelte'; import CellHeader from './cell-header.svelte';
import { colToStr, getEvalLiteral, refToStr, type CellT } from './utils'; import { colToStr, getEvalLiteral, isErr, refToStr, type CellT } from './utils';
import clsx from 'clsx'; import clsx from 'clsx';
let { let {
@@ -44,7 +44,7 @@
console.error('Expected cell value for SET msgponse from server.'); console.error('Expected cell value for SET msgponse from server.');
return; return;
} }
setCellVal(msg.cell.row, msg.cell.col, getEvalLiteral(msg.eval)); setCellVal(msg.cell.row, msg.cell.col, getEvalLiteral(msg.eval), isErr(msg.eval));
break; break;
} }
case 'bulk': { case 'bulk': {
@@ -107,38 +107,41 @@
const key = (i: number, j: number) => `${i}:${j}`; const key = (i: number, j: number) => `${i}:${j}`;
const getCell = (i: number, j: number) => grid_vals[key(i, j)] ?? undefined; const getCell = (i: number, j: number) => grid_vals[key(i, j)];
const getCellRaw = (i: number, j: number) => getCell(i, j)?.raw_val ?? ''; const getCellRaw = (i: number, j: number) => getCell(i, j)?.raw_val ?? '';
const setCellRaw = (i: number, j: number, val: string) => { const setCellRaw = (i: number, j: number, val: string) => {
if (grid_vals[key(i, j)] === undefined) { if (grid_vals[key(i, j)] === undefined) {
grid_vals[key(i, j)] = { grid_vals[key(i, j)] = {
raw_val: val, raw_val: val,
isErr: false,
val: undefined val: undefined
}; };
} else { } else {
grid_vals[key(i, j)].raw_val = val; grid_vals[key(i, j)].raw_val = val;
} }
}; };
const getCellVal = (i: number, j: number) => getCell(i, j)?.val ?? undefined; const getCellVal = (i: number, j: number) => getCell(i, j);
const setCellVal = (i: number, j: number, val: LiteralValue) => { const setCellVal = (i: number, j: number, val: LiteralValue, isErr: boolean) => {
if (grid_vals[key(i, j)] === undefined) { if (grid_vals[key(i, j)] === undefined) {
console.warn('Cell raw value was undefined but recieved eval.'); console.warn('Cell raw value was undefined but recieved eval.');
} else { } else {
grid_vals[key(i, j)].val = val; let cell = grid_vals[key(i, j)];
cell.val = val;
cell.isErr = isErr;
} }
}; };
const setCell = (row: number, col: number, v: string | undefined) => { const setCell = (row: number, col: number, v: CellT | undefined) => {
// ignore “no value” so we dont create keys on mount // ignore “no value” so we dont create keys on mount
if (v == null || v === '') delete grid_vals[key(row, col)]; if (v?.raw_val == null || v.raw_val === '') delete grid_vals[key(row, col)];
else { else {
setCellRaw(row, col, v); setCellRaw(row, col, v.raw_val);
let msg: LeadMsg = { let msg: LeadMsg = {
msg_type: 'set', msg_type: 'set',
cell: { row, col }, cell: { row, col },
raw: v raw: v.raw_val
}; };
socket.send(JSON.stringify(msg)); socket.send(JSON.stringify(msg));
@@ -237,8 +240,7 @@
onmousedown={(e) => { onmousedown={(e) => {
handleCellInteraction(i, j, e); handleCellInteraction(i, j, e);
}} }}
bind:raw_val={() => getCellRaw(i, j), (v) => setCell(i, j, v)} bind:cell={() => getCell(i, j), (v) => setCell(i, j, v)}
val={getCellVal(i, j)}
active={active_cell !== null && active_cell[0] === i && active_cell[1] === j} active={active_cell !== null && active_cell[0] === i && active_cell[1] === j}
/> />
{/each} {/each}

View File

@@ -25,5 +25,16 @@ interface EvalCellRef {
reference: CellRef; reference: CellRef;
} }
interface LeadErr {
code: 'DivZero' | 'TypeErr' | 'Syntax' | 'Server' | 'Unsupported';
desc: string;
title: string;
}
// Tagged union // Tagged union
type Eval = { literal: Literal } | { cellref: EvalCellRef } | { range: Range } | 'unset'; type Eval =
| { literal: Literal }
| { cellref: EvalCellRef }
| { range: Range }
| { err: LeadErr }
| 'unset';

View File

@@ -1,6 +1,7 @@
export interface CellT { export interface CellT {
raw_val: string; raw_val: string;
val: LiteralValue | undefined; val: LiteralValue | undefined;
isErr: boolean;
} }
/** /**
@@ -47,6 +48,12 @@ export function getEvalLiteral(value: Eval): LiteralValue {
if (value === 'unset') return ''; if (value === 'unset') return '';
if ('literal' in value) return value.literal.value; if ('literal' in value) return value.literal.value;
if ('cellref' in value) return getEvalLiteral(value.cellref.eval); if ('cellref' in value) return getEvalLiteral(value.cellref.eval);
if ('err' in value) return `err: ${value.err.code}`;
// if ('range' in value) return 'err'; // if ('range' in value) return 'err';
return 'todo!'; return 'todo!';
} }
export function isErr(value: Eval): boolean {
if (value === 'unset') return false;
return 'err' in value;
}