From 4653ffebb43d733a3cff873d0903c7d00aaeb499 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 23 Jan 2023 15:03:10 +0100 Subject: Math module --- src/ide/complete.rs | 7 +++---- src/ide/tooltip.rs | 14 +++++++------- src/model/eval.rs | 25 ++++++++++++++++--------- src/model/library.rs | 10 ++++++---- src/model/module.rs | 22 ++++++++++++++++------ src/model/scope.rs | 44 ++++++++++++++++++++++++++++++++------------ src/syntax/ast.rs | 2 +- src/syntax/parser.rs | 7 +++++++ 8 files changed, 88 insertions(+), 43 deletions(-) (limited to 'src') diff --git a/src/ide/complete.rs b/src/ide/complete.rs index f0808b21..5b044746 100644 --- a/src/ide/complete.rs +++ b/src/ide/complete.rs @@ -4,7 +4,6 @@ use if_chain::if_chain; use super::{plain_docs_sentence, summarize_font_family}; use crate::model::{CastInfo, Scope, Value}; -use crate::syntax::ast::AstNode; use crate::syntax::{ast, LinkedNode, Source, SyntaxKind}; use crate::util::{format_eco, EcoString}; use crate::World; @@ -118,8 +117,8 @@ fn complete_params(ctx: &mut CompletionContext) -> bool { if let Some(grand) = parent.parent(); if let Some(expr) = grand.cast::(); let set = matches!(expr, ast::Expr::Set(_)); - if let Some(callee) = match expr { - ast::Expr::FuncCall(call) => call.callee().as_untyped().cast(), + if let Some(ast::Expr::Ident(callee)) = match expr { + ast::Expr::FuncCall(call) => Some(call.callee()), ast::Expr::Set(set) => Some(set.target()), _ => None, }; @@ -377,7 +376,7 @@ impl<'a> CompletionContext<'a> { let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?; Some(Self { world, - scope: &world.library().scope, + scope: &world.library().global.scope(), before: &text[..cursor], after: &text[cursor..], leaf, diff --git a/src/ide/tooltip.rs b/src/ide/tooltip.rs index 0d46695b..076e2b45 100644 --- a/src/ide/tooltip.rs +++ b/src/ide/tooltip.rs @@ -2,7 +2,7 @@ use if_chain::if_chain; use super::{plain_docs_sentence, summarize_font_family}; use crate::model::{CastInfo, Value}; -use crate::syntax::ast::{self, AstNode}; +use crate::syntax::ast; use crate::syntax::{LinkedNode, Source, SyntaxKind}; use crate::World; @@ -23,7 +23,7 @@ fn function_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option { leaf.parent_kind(), Some(SyntaxKind::FuncCall | SyntaxKind::SetRule), ); - if let Some(Value::Func(func)) = world.library().scope.get(&ident); + if let Some(Value::Func(func)) = world.library().global.scope().get(&ident); if let Some(info) = func.info(); then { return Some(plain_docs_sentence(info.docs)); @@ -44,14 +44,14 @@ fn named_param_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option { if matches!(grand.kind(), SyntaxKind::Args); if let Some(grand_grand) = grand.parent(); if let Some(expr) = grand_grand.cast::(); - if let Some(callee) = match expr { - ast::Expr::FuncCall(call) => call.callee().as_untyped().cast(), + if let Some(ast::Expr::Ident(callee)) = match expr { + ast::Expr::FuncCall(call) => Some(call.callee()), ast::Expr::Set(set) => Some(set.target()), _ => None, }; // Find metadata about the function. - if let Some(Value::Func(func)) = world.library().scope.get(&callee); + if let Some(Value::Func(func)) = world.library().global.scope().get(&callee); if let Some(info) = func.info(); then { (info, named) } else { return None; } @@ -103,8 +103,8 @@ fn font_family_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option { if matches!(parent.kind(), SyntaxKind::Args); if let Some(grand) = parent.parent(); if let Some(expr) = grand.cast::(); - if let Some(callee) = match expr { - ast::Expr::FuncCall(call) => call.callee().as_untyped().cast(), + if let Some(ast::Expr::Ident(callee)) = match expr { + ast::Expr::FuncCall(call) => Some(call.callee()), ast::Expr::Set(set) => Some(set.target()), _ => None, }; diff --git a/src/model/eval.rs b/src/model/eval.rs index 67c733ce..6de328bc 100644 --- a/src/model/eval.rs +++ b/src/model/eval.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use std::mem; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use comemo::{Track, Tracked}; use unicode_segmentation::UnicodeSegmentation; @@ -32,9 +32,9 @@ pub fn eval( ) -> SourceResult { // Prevent cyclic evaluation. let id = source.id(); + let path = if id.is_detached() { Path::new("") } else { world.source(id).path() }; if route.contains(id) { - let path = world.source(id).path().display(); - panic!("Tried to cyclicly evaluate {}", path); + panic!("Tried to cyclicly evaluate {}", path.display()); } // Hook up the lang items. @@ -43,7 +43,7 @@ pub fn eval( // Evaluate the module. let route = unsafe { Route::insert(route, id) }; - let scopes = Scopes::new(Some(&library.scope)); + let scopes = Scopes::new(Some(library)); let mut vm = Vm::new(world, route.track(), id, scopes, 0); let result = source.ast()?.eval(&mut vm); @@ -53,7 +53,8 @@ pub fn eval( } // Assemble the module. - Ok(Module::evaluated(source.path(), vm.scopes.top, result?)) + let name = path.file_stem().unwrap_or_default().to_string_lossy(); + Ok(Module::new(name).with_scope(vm.scopes.top).with_content(result?)) } /// A virtual machine. @@ -521,7 +522,7 @@ impl Eval for ast::Math { .map(|expr| expr.eval_in_math(vm)) .collect::>()?; let block = self.block(); - Ok((vm.items.math)(Content::sequence(seq), block)) + Ok((vm.items.math_formula)(Content::sequence(seq), block)) } } @@ -608,11 +609,11 @@ impl Eval for ast::Ident { impl ast::Ident { fn eval_in_math(&self, vm: &mut Vm) -> SourceResult { if self.as_untyped().len() == self.len() - && matches!(vm.scopes.get(self), Ok(Value::Func(_)) | Err(_)) + && matches!(vm.scopes.get_in_math(self), Ok(Value::Func(_)) | Err(_)) { Ok((vm.items.symbol)(EcoString::from(self.get()) + ":op".into())) } else { - Ok(self.eval(vm)?.display_in_math()) + Ok(vm.scopes.get_in_math(self).at(self.span())?.clone().display_in_math()) } } } @@ -933,7 +934,13 @@ impl Eval for ast::FuncCall { impl ast::FuncCall { fn eval_in_math(&self, vm: &mut Vm) -> SourceResult { - let callee = self.callee().eval(vm)?; + let callee = match self.callee() { + ast::Expr::Ident(ident) => { + vm.scopes.get_in_math(&ident).at(ident.span())?.clone() + } + expr => expr.eval(vm)?, + }; + if let Value::Func(callee) = callee { let args = self.args().eval(vm)?; Ok(Self::eval_call(vm, &callee, args, self.span())?.display_in_math()) diff --git a/src/model/library.rs b/src/model/library.rs index 54eeeb5b..a64b0263 100644 --- a/src/model/library.rs +++ b/src/model/library.rs @@ -4,7 +4,7 @@ use std::num::NonZeroUsize; use once_cell::sync::OnceCell; -use super::{Content, NodeId, Scope, StyleChain, StyleMap, Vt}; +use super::{Content, Module, NodeId, StyleChain, StyleMap, Vt}; use crate::diag::SourceResult; use crate::doc::Document; use crate::geom::{Abs, Dir}; @@ -14,7 +14,9 @@ use crate::util::{hash128, EcoString}; #[derive(Debug, Clone, Hash)] pub struct Library { /// The scope containing definitions that are available everywhere. - pub scope: Scope, + pub global: Module, + /// The scope containing definitions available in math mode. + pub math: Module, /// The default properties for page size, font selection and so on. pub styles: StyleMap, /// Defines which standard library items fulfill which syntactical roles. @@ -66,7 +68,7 @@ pub struct LangItems { /// An item in a term list: `/ Term: Details`. pub term_item: fn(term: Content, description: Content) -> Content, /// A mathematical formula: `$x$`, `$ x^2 $`. - pub math: fn(body: Content, block: bool) -> Content, + pub math_formula: fn(body: Content, block: bool) -> Content, /// A subsection in a math formula that is surrounded by matched delimiters: /// `[x + y]`. pub math_delimited: fn(body: Content) -> Content, @@ -106,7 +108,7 @@ impl Hash for LangItems { self.list_item.hash(state); self.enum_item.hash(state); self.term_item.hash(state); - self.math.hash(state); + self.math_formula.hash(state); self.math_atom.hash(state); self.math_script.hash(state); self.math_frac.hash(state); diff --git a/src/model/module.rs b/src/model/module.rs index ba6c76fb..6a1c60a5 100644 --- a/src/model/module.rs +++ b/src/model/module.rs @@ -1,5 +1,4 @@ use std::fmt::{self, Debug, Formatter}; -use std::path::Path; use std::sync::Arc; use super::{Content, Scope, Value}; @@ -22,7 +21,7 @@ struct Repr { } impl Module { - /// Create a new, empty module with the given `name`. + /// Create a new module. pub fn new(name: impl Into) -> Self { Self(Arc::new(Repr { name: name.into(), @@ -31,10 +30,16 @@ impl Module { })) } - /// Create a new module from an evalauted file. - pub fn evaluated(path: &Path, scope: Scope, content: Content) -> Self { - let name = path.file_stem().unwrap_or_default().to_string_lossy().into(); - Self(Arc::new(Repr { name, scope, content })) + /// Update the module's scope. + pub fn with_scope(mut self, scope: Scope) -> Self { + Arc::make_mut(&mut self.0).scope = scope; + self + } + + /// Update the module's content. + pub fn with_content(mut self, content: Content) -> Self { + Arc::make_mut(&mut self.0).content = content; + self } /// Get the module's name. @@ -47,6 +52,11 @@ impl Module { &self.0.scope } + /// Access the module's scope, mutably. + pub fn scope_mut(&mut self) -> &mut Scope { + &mut Arc::make_mut(&mut self.0).scope + } + /// Try to access a definition in the module. pub fn get(&self, name: &str) -> StrResult<&Value> { self.scope().get(&name).ok_or_else(|| { diff --git a/src/model/scope.rs b/src/model/scope.rs index c54cf1b3..bb0d4684 100644 --- a/src/model/scope.rs +++ b/src/model/scope.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use std::fmt::{self, Debug, Formatter}; use std::hash::Hash; -use super::{Func, FuncType, Value}; +use super::{Func, FuncType, Library, Value}; use crate::diag::StrResult; use crate::util::EcoString; @@ -13,13 +13,13 @@ pub struct Scopes<'a> { pub top: Scope, /// The stack of lower scopes. pub scopes: Vec, - /// The base scope. - pub base: Option<&'a Scope>, + /// The standard library. + pub base: Option<&'a Library>, } impl<'a> Scopes<'a> { /// Create a new, empty hierarchy of scopes. - pub fn new(base: Option<&'a Scope>) -> Self { + pub fn new(base: Option<&'a Library>) -> Self { Self { top: Scope::new(), scopes: vec![], base } } @@ -39,7 +39,16 @@ impl<'a> Scopes<'a> { pub fn get(&self, var: &str) -> StrResult<&Value> { Ok(std::iter::once(&self.top) .chain(self.scopes.iter().rev()) - .chain(self.base.into_iter()) + .chain(self.base.map(|base| base.global.scope())) + .find_map(|scope| scope.get(var)) + .ok_or("unknown variable")?) + } + + /// Try to access a variable immutably from within a math formula. + pub fn get_in_math(&self, var: &str) -> StrResult<&Value> { + Ok(std::iter::once(&self.top) + .chain(self.scopes.iter().rev()) + .chain(self.base.map(|base| base.math.scope())) .find_map(|scope| scope.get(var)) .ok_or("unknown variable")?) } @@ -50,10 +59,9 @@ impl<'a> Scopes<'a> { .chain(&mut self.scopes.iter_mut().rev()) .find_map(|scope| scope.get_mut(var)) .ok_or_else(|| { - if self.base.map_or(false, |base| base.get(var).is_some()) { - "cannot mutate a constant" - } else { - "unknown variable" + match self.base.and_then(|base| base.global.scope().get(var)) { + Some(_) => "cannot mutate a constant", + _ => "unknown variable", } })? } @@ -61,17 +69,29 @@ impl<'a> Scopes<'a> { /// A map from binding names to values. #[derive(Default, Clone, Hash)] -pub struct Scope(BTreeMap); +pub struct Scope(BTreeMap, bool); impl Scope { /// Create a new empty scope. pub fn new() -> Self { - Self::default() + Self(BTreeMap::new(), false) + } + + /// Create a new scope with duplication prevention. + pub fn deduplicating() -> Self { + Self(BTreeMap::new(), true) } /// Bind a value to a name. pub fn define(&mut self, name: impl Into, value: impl Into) { - self.0.insert(name.into(), Slot::new(value.into(), Kind::Normal)); + let name = name.into(); + + #[cfg(debug_assertions)] + if self.1 && self.0.contains_key(&name) { + panic!("duplicate definition: {name}"); + } + + self.0.insert(name, Slot::new(value.into(), Kind::Normal)); } /// Define a function through a native rust function. diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs index ceda2d57..4bab0c42 100644 --- a/src/syntax/ast.rs +++ b/src/syntax/ast.rs @@ -1505,7 +1505,7 @@ node! { impl SetRule { /// The function to set style properties for. - pub fn target(&self) -> Ident { + pub fn target(&self) -> Expr { self.0.cast_first_match().expect("set rule is missing target") } diff --git a/src/syntax/parser.rs b/src/syntax/parser.rs index 15839e18..07f53372 100644 --- a/src/syntax/parser.rs +++ b/src/syntax/parser.rs @@ -712,7 +712,14 @@ fn let_binding(p: &mut Parser) { fn set_rule(p: &mut Parser) { let m = p.marker(); p.assert(SyntaxKind::Set); + + let m2 = p.marker(); p.expect(SyntaxKind::Ident); + while p.eat_if(SyntaxKind::Dot) { + p.expect(SyntaxKind::Ident); + p.wrap(m2, SyntaxKind::FieldAccess); + } + args(p); if p.eat_if(SyntaxKind::If) { code_expr(p); -- cgit v1.2.3