use crate::{ bitmap::MapPoint, brush::{Brush, RectSelectBrush}, grid::GridKind, guide::Guide, lisp::{ error::{EvalError, LispError}, expr::{Arity, LispExpr}, number::LispNumber, Environment, }, primitive, undo::PaintRecord, utils::{load_script, rect_coords}, }; use std::{convert::TryInto, fs::File, io::BufWriter, path::Path}; #[macro_export] macro_rules! primitive { ($env:expr, $arity:expr, $name:expr, $closure:expr) => { let val = crate::lisp::expr::LispExpr::PrimitiveFunc(crate::lisp::expr::PrimitiveFunc { arity: $arity, closure: $closure, name: $name, }); let _ = $env.insert($name.to_string(), val); }; } #[macro_export] macro_rules! define { ($env:expr, $name:expr, $value:expr) => { let _ = $env.insert($name.to_string(), $value); }; } #[macro_export] macro_rules! type_match { ($args:expr, $($range:literal => $kind:pat),+) => { { let mut temp_vec = vec![]; $( temp_vec.push(matches!(&$args[$range], $kind)); )+ temp_vec.iter().all(|&t| t) } }; ($args:expr, $($range:expr => $kind:pat),+) => { { let mut temp_vec = vec![]; $( let arg_range: &[LispExpr] = &$args[$range]; for arg in arg_range { temp_vec.push(matches!(arg, $kind)); } )+ temp_vec.iter().all(|&t| t) } } } pub fn new_env() -> Result { let mut env = Environment::new(); define!(env, "else", LispExpr::BoolLit(true)); primitive!(env, Arity::Atleast(2), "+", |args, _| { let nums = args .iter() .map(|arg| arg.try_into()) .collect::, LispError>>()?; return Ok(LispExpr::Number( nums.iter().fold(LispNumber::Integer(0), |acc, &x| acc + *x), )); }); primitive!(env, Arity::Atleast(2), "-", |args, _| { let nums = args .iter() .map(|arg| arg.try_into()) .collect::, LispError>>()?; let mut acc = *nums[0]; for arg in nums.into_iter().skip(1) { acc = acc - *arg; } Ok(LispExpr::Number(acc)) }); primitive!(env, Arity::Atleast(2), "*", |args, _| { let nums = args .iter() .map(|arg| arg.try_into()) .collect::, LispError>>()?; return Ok(LispExpr::Number( nums.iter().fold(LispNumber::Integer(1), |acc, &x| acc * *x), )); }); primitive!(env, Arity::Atleast(2), "/", |args, _| { let nums = args .iter() .map(|arg| arg.try_into()) .collect::, LispError>>()?; let mut acc = *nums[0]; for arg in nums.into_iter().skip(1) { acc = acc.div(*arg)?; } Ok(LispExpr::Number(acc)) }); primitive!(env, Arity::Atleast(2), "and", |args, _| { if args .iter() .any(|arg| matches!(arg, LispExpr::BoolLit(false))) { Ok(LispExpr::BoolLit(false)) } else { Ok(LispExpr::BoolLit(true)) } }); primitive!(env, Arity::Atleast(2), "or", |args, _| { if args .iter() .any(|arg| matches!(arg, LispExpr::BoolLit(true))) { Ok(LispExpr::BoolLit(true)) } else { Ok(LispExpr::BoolLit(false)) } }); primitive!(env, Arity::Exact(1), "not", |args, _| { if type_match!(args, 0 => LispExpr::BoolLit(false)) { Ok(LispExpr::BoolLit(true)) } else { Ok(LispExpr::BoolLit(false)) } }); primitive!(env, Arity::Exact(1), "integer", |args, _| { if type_match!(args, 0 => LispExpr::Number(_)) { Ok(LispExpr::Number(match args[0].unwrap_number() { LispNumber::Float(f) => LispNumber::Integer(f as i64), s => s, })) } else { Err(EvalError::TypeMismatch.into()) } }); primitive!(env, Arity::Atleast(1), "begin", |args, _| { Ok(args.iter().last().unwrap().clone()) }); primitive!(env, Arity::Exact(0), "quit", |_, app| { app.quit(); Ok(LispExpr::Unit) }); primitive!(env, Arity::Exact(2), "eq?", |args, _| { let s = &args[0]; let o = &args[1]; match (s, o) { (LispExpr::Unit, LispExpr::Unit) => Ok(true), (LispExpr::Number(s), LispExpr::Number(o)) => Ok(s == o), (LispExpr::List(s), LispExpr::List(o)) => Ok(s.iter().zip(o).all(|(a, b)| a == b)), (LispExpr::DottedList(s), LispExpr::DottedList(o)) => { Ok(s.iter().zip(o).all(|(a, b)| a == b)) } (LispExpr::List(s), LispExpr::Unit) => Ok(s.is_empty()), (LispExpr::Unit, LispExpr::List(s)) => Ok(s.is_empty()), (LispExpr::DottedList(_), LispExpr::Unit) => Ok(false), (LispExpr::Unit, LispExpr::DottedList(_)) => Ok(false), (LispExpr::StringLit(s), LispExpr::StringLit(o)) => Ok(s == o), (LispExpr::Char(s), LispExpr::Char(o)) => Ok(s == o), (LispExpr::BoolLit(s), LispExpr::BoolLit(o)) => Ok(s == o), (LispExpr::Ident(s), LispExpr::Ident(o)) => Ok(s == o), (s @ LispExpr::Quote(_, _), o @ LispExpr::Quote(_, _)) => { // text of quotation should be equal Ok(format!("{}", s) == format!("{}", o)) } _ => Err(EvalError::TypeMismatch.into()), } .map(LispExpr::BoolLit) }); primitive!(env, Arity::Atleast(2), ">", |args, _| { let nums = args .iter() .map(|arg| arg.try_into()) .collect::, LispError>>()?; let acc = *nums[0]; Ok(LispExpr::BoolLit( nums.into_iter().skip(1).all(|&arg| acc > arg), )) }); primitive!(env, Arity::Atleast(2), ">=", |args, _| { let nums = args .iter() .map(|arg| arg.try_into()) .collect::, LispError>>()?; let acc = *nums[0]; Ok(LispExpr::BoolLit( nums.into_iter().skip(1).all(|&arg| acc >= arg), )) }); primitive!(env, Arity::Atleast(2), "<", |args, _| { let nums = args .iter() .map(|arg| arg.try_into()) .collect::, LispError>>()?; let acc = *nums[0]; Ok(LispExpr::BoolLit( nums.into_iter().skip(1).all(|&arg| acc < arg), )) }); primitive!(env, Arity::Atleast(2), "<=", |args, _| { let nums = args .iter() .map(|arg| arg.try_into()) .collect::, LispError>>()?; let acc = *nums[0]; Ok(LispExpr::BoolLit( nums.into_iter().skip(1).all(|&arg| acc <= arg), )) }); primitive!(env, Arity::Exact(1), "string-len", |args, _| { if type_match!(args, 0 => LispExpr::StringLit(_)) { Ok(LispExpr::Number(LispNumber::Integer( args[0].unwrap_stringlit().len() as i64, ))) } else { Err(EvalError::TypeMismatch.into()) } }); primitive!(env, Arity::Exact(0), "save", |_, app| { if let Some(file_name) = &app.file_name { return app .save_as(&file_name) .map_err(|e| LispError::Stringified(e.to_string())) .map(|_| LispExpr::Unit); } Err(EvalError::NoFileName.into()) }); primitive!(env, Arity::Exact(1), "save-as", |args, app| { match &args[0] { LispExpr::StringLit(s) => app .save_as(&s) .map_err(|e| LispError::Stringified(e.to_string())) .map(|_| LispExpr::Unit), _ => Err(EvalError::TypeMismatch.into()), } }); primitive!(env, Arity::Exact(1), "export-png", |args, app| { if let LispExpr::StringLit(s) = &args[0] { let export_path = Path::new(&s); let file = File::create(export_path).map_err(EvalError::FileError)?; let w = BufWriter::new(file); app.export().write_png(w); Ok(LispExpr::Unit) } else { Err(EvalError::TypeMismatch.into()) } }); primitive!(env, Arity::Exact(0), "grid-enabled?", |_, app| { Ok(LispExpr::BoolLit(app.grid.enabled)) }); primitive!(env, Arity::Exact(1), "set-grid!", |args, app| { app.grid.enabled = args[0].cast_bool(); Ok(LispExpr::Unit) }); primitive!(env, Arity::Exact(0), "minimap-enabled?", |_, app| { Ok(LispExpr::BoolLit(app.minimap)) }); primitive!(env, Arity::Exact(1), "set-minimap!", |args, app| { app.minimap = args[0].cast_bool(); Ok(LispExpr::Unit) }); primitive!(env, Arity::Exact(0), "grid-rectangle", |_, app| { app.grid.kind = GridKind::Rectangle; Ok(LispExpr::Unit) }); primitive!(env, Arity::Exact(0), "grid-isometric", |_, app| { app.grid.kind = GridKind::Isometric; Ok(LispExpr::Unit) }); primitive!(env, Arity::Exact(0), "brush-fill", |_, app| { app.brush = Brush::Fill; Ok(LispExpr::Unit) }); primitive!(env, Arity::Exact(0), "brush-circle", |_, app| { app.brush = Brush::new(0); Ok(LispExpr::Unit) }); primitive!(env, Arity::Exact(0), "brush-line", |_, app| { app.brush = Brush::line(0, false); Ok(LispExpr::Unit) }); primitive!(env, Arity::Exact(0), "brush-line-extend", |_, app| { app.brush = Brush::line(0, true); Ok(LispExpr::Unit) }); primitive!(env, Arity::Exact(2), "cons", |args, _| { if type_match!(args, 1 => LispExpr::Unit) { Ok(LispExpr::List(vec![args[0].clone()])) } else if type_match!(args, 1 => LispExpr::DottedList(_)) { // cons of anything to an improper list is an improper list let mut rest = args[1].unwrap_dotted_list(); rest.insert(0, args[0].clone()); Ok(LispExpr::DottedList(rest)) } else if type_match!(args, 1 => LispExpr::List(_)) { // cons of anything to a proper list is a proper list let mut rest = args[1].unwrap_list(); rest.insert(0, args[0].clone()); Ok(LispExpr::List(rest)) } else { // attempt to cons non-lists Ok(LispExpr::DottedList(vec![args[0].clone(), args[1].clone()])) } }); primitive!(env, Arity::Exact(1), "car", |args, _| { if type_match!(args, 0 => LispExpr::List(_)) { Ok(args[0].unwrap_list().swap_remove(0)) } else if type_match!(args, 0 => LispExpr::DottedList(_)) { Ok(args[0].unwrap_dotted_list().swap_remove(0)) } else if type_match!(args, 0 => LispExpr::Unit) { Err(EvalError::AccessEmptyList.into()) } else { Err(EvalError::TypeMismatch.into()) } }); primitive!(env, Arity::Exact(1), "cdr", |args, _| { if type_match!(args, 0 => LispExpr::List(_)) { // cdr of a proper list is a proper list let mut ls = args[0].unwrap_list(); if ls.is_empty() { Err(EvalError::AccessEmptyList.into()) } else if ls.len() == 1 { Ok(LispExpr::Unit) } else { ls.remove(0); Ok(LispExpr::List(ls)) } } else if type_match!(args, 0 => LispExpr::DottedList(_)) { // cdr of an improper list is an improper list or an atom let ls = args[0].unwrap_dotted_list(); if ls.len() == 2 { Ok(ls.into_iter().last().unwrap()) } else { // should be unreachable Err(EvalError::AccessEmptyList.into()) } } else if type_match!(args, 0 => LispExpr::Unit) { Err(EvalError::AccessEmptyList.into()) } else { Err(EvalError::TypeMismatch.into()) } }); primitive!(env, Arity::Atleast(1), "list", |args, _| { Ok(LispExpr::List(args.to_vec())) }); primitive!(env, Arity::Exact(1), "load-script", |args, app| { if type_match!(args, 0 => LispExpr::StringLit(_)) { let path = args[0].unwrap_stringlit(); load_script(&path, app).map(|_| LispExpr::Unit) } else { Err(EvalError::TypeMismatch.into()) } }); primitive!(env, Arity::Atleast(1), "error", |args, _| { if type_match!(args, 0 => LispExpr::StringLit(_)) { let mut s = args[0].unwrap_stringlit(); for arg in args.iter().skip(1) { s.push_str(&format!(" {}", arg)); } Err(EvalError::Custom(s).into()) } else { Err(EvalError::TypeMismatch.into()) } }); primitive!(env, Arity::Exact(2), "assert-eq", |args, app| { if args[0].compare(&args[1], &app.lisp_env)? { Ok(LispExpr::Unit) } else { Err(EvalError::AssertionError { expected: args[0].clone(), got: args[1].clone(), } .into()) } }); primitive!(env, Arity::Exact(0), "canvas-width", |_, app| { Ok(LispExpr::Number(LispNumber::Integer(app.width() as i64))) }); primitive!(env, Arity::Exact(0), "canvas-height", |_, app| { Ok(LispExpr::Number(LispNumber::Integer(app.height() as i64))) }); primitive!(env, Arity::Exact(3), "set-pixel!", |args, app| { if type_match!( args, 0 => LispExpr::Number(LispNumber::Integer(_)), 1 => LispExpr::Number(LispNumber::Integer(_)), 2 => LispExpr::BoolLit(_) ) { let x = args[0].unwrap_number(); let y = args[1].unwrap_number(); let val = args[2].cast_bool(); let set_loc: MapPoint = (x.unwrap_integer(), y.unwrap_integer()) .try_into() .map_err(|_| -> LispError { EvalError::InvalidCoordinates((x, y)).into() })?; if !app.pixmap.contains(set_loc) { Err(EvalError::InvalidCoordinates((x, y)).into()) } else { let old_val = app.pixmap.set(set_loc, val); app.current_operation .push(PaintRecord::new(set_loc, old_val, val)); Ok(LispExpr::Unit) } } else { Err(EvalError::TypeMismatch.into()) } }); primitive!(env, Arity::Exact(2), "get-pixel", |args, app| { if type_match!( args, 0 => LispExpr::Number(LispNumber::Integer(_)), 1 => LispExpr::Number(LispNumber::Integer(_)) ) { let x = args[0].unwrap_number(); let y = args[1].unwrap_number(); let get_loc: MapPoint = (x.unwrap_integer(), y.unwrap_integer()) .try_into() .map_err(|_| -> LispError { EvalError::InvalidCoordinates((x, y)).into() })?; if !app.pixmap.contains(get_loc) { Err(EvalError::InvalidCoordinates((x, y)).into()) } else { Ok(LispExpr::BoolLit(app.pixmap.get(get_loc))) } } else { Err(EvalError::TypeMismatch.into()) } }); primitive!(env, Arity::Exact(0), "commit", |_, app| { app.commit_operation(); Ok(LispExpr::Unit) }); primitive!(env, Arity::Exact(2), "add-guide!", |args, app| { match args { [axis, LispExpr::Number(LispNumber::Integer(offset))] => { let guide = Guide { axis: axis.try_into()?, offset: *offset as u32, }; app.guides.insert(guide, true); Ok(LispExpr::Unit) } _ => Err(EvalError::TypeMismatch.into()), } }); primitive!(env, Arity::Exact(0), "selection-start", |_, app| { if let Brush::RectSelect(RectSelectBrush { start: Some(s), end: Some(e), .. }) = app.brush { let pt = rect_coords(s, e).0; Ok(LispExpr::DottedList(vec![ (pt.x as i64).into(), (pt.y as i64).into(), ])) } else { Err(EvalError::CustomInternal("No active selection!").into()) } }); primitive!(env, Arity::Exact(0), "selection-end", |_, app| { if let Brush::RectSelect(RectSelectBrush { start: Some(s), end: Some(e), .. }) = app.brush { let pt = rect_coords(s, e).1; Ok(LispExpr::DottedList(vec![ (pt.x as i64).into(), (pt.y as i64).into(), ])) } else { Err(EvalError::CustomInternal("No active selection!").into()) } }); primitive!(env, Arity::Exact(2), "range", |args, _| { if type_match!( args, 0 => LispExpr::Number(LispNumber::Integer(_)), 1 => LispExpr::Number(LispNumber::Integer(_))) { let lower = args[0].unwrap_number().unwrap_integer(); let upper = args[1].unwrap_number().unwrap_integer(); Ok(LispExpr::List( (lower..upper) .map(|i| LispExpr::Number(LispNumber::Integer(i))) .collect::>(), )) } else { Err(EvalError::TypeMismatch.into()) } }); primitive!(env, Arity::Exact(1), "id", |args, _| { Ok(args[0].clone()) }); Ok(env) }