Make symbols callable like functions & migrate callable accents to callable symbols (#4299)

Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
Yip Coekjan 2024-06-14 00:57:34 +08:00 committed by GitHub
parent ad4ef68a11
commit 6f9855a8c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 252 additions and 111 deletions

View File

@ -340,17 +340,37 @@ pub fn derive_cast(item: BoundaryStream) -> BoundaryStream {
/// Defines a list of `Symbol`s.
///
/// The `#[call(path)]` attribute can be used to specify a function to call when
/// the symbol is invoked. The function must be `NativeFunc`.
///
/// ```ignore
/// const EMOJI: &[(&str, Symbol)] = symbols! {
/// // A plain symbol without modifiers.
/// abacus: '🧮',
/// // A plain symbol without modifiers.
/// abacus: '🧮',
///
/// // A symbol with a modifierless default and one modifier.
/// alien: ['👽', monster: '👾'],
/// // A symbol with a modifierless default and one modifier.
/// alien: ['👽', monster: '👾'],
///
/// // A symbol where each variant has a modifier. The first one will be
/// // the default.
/// clock: [one: '🕐', two: '🕑', ...],
/// // A symbol where each variant has a modifier. The first one will be
/// // the default.
/// clock: [one: '🕐', two: '🕑', ...],
///
/// // A callable symbol without modifiers.
/// breve: #[call(crate::math::breve)] '˘',
///
/// // A callable symbol with a modifierless default and one modifier.
/// acute: [
/// #[call(crate::math::acute)] '´',
/// double: '˝',
/// ],
///
/// // A callable symbol where each variant has a modifier.
/// arrow: [
/// #[call(crate::math::arrow)] r: '→',
/// r.long.bar: '⟼',
/// #[call(crate::math::arrow_l)] l: '←',
/// l.long.bar: '⟻',
/// ],
/// }
/// ```
///

View File

@ -3,7 +3,7 @@ use quote::quote;
use syn::ext::IdentExt;
use syn::parse::{Parse, ParseStream, Parser};
use syn::punctuated::Punctuated;
use syn::{Ident, Result, Token};
use syn::{Ident, LitChar, Path, Result, Token};
/// Expand the `symbols!` macro.
pub fn symbols(stream: TokenStream) -> Result<TokenStream> {
@ -12,12 +12,16 @@ pub fn symbols(stream: TokenStream) -> Result<TokenStream> {
let pairs = list.iter().map(|symbol| {
let name = symbol.name.to_string();
let kind = match &symbol.kind {
Kind::Single(c) => quote! { ::typst::symbols::Symbol::single(#c), },
Kind::Single(c, h) => {
let symbol = construct_sym_char(c, h);
quote! { ::typst::symbols::Symbol::single(#symbol), }
}
Kind::Multiple(variants) => {
let variants = variants.iter().map(|variant| {
let name = &variant.name;
let c = &variant.c;
quote! { (#name, #c) }
let symbol = construct_sym_char(c, &variant.handler);
quote! { (#name, #symbol) }
});
quote! {
::typst::symbols::Symbol::list(&[#(#variants),*])
@ -29,21 +33,36 @@ pub fn symbols(stream: TokenStream) -> Result<TokenStream> {
Ok(quote! { &[#(#pairs),*] })
}
fn construct_sym_char(ch: &LitChar, handler: &Handler) -> TokenStream {
match &handler.0 {
None => quote! { ::typst::symbols::SymChar::pure(#ch), },
Some(path) => quote! {
::typst::symbols::SymChar::with_func(
#ch,
<#path as ::typst::foundations::NativeFunc>::func,
),
},
}
}
struct Symbol {
name: syn::Ident,
kind: Kind,
}
enum Kind {
Single(syn::LitChar),
Single(syn::LitChar, Handler),
Multiple(Punctuated<Variant, Token![,]>),
}
struct Variant {
name: String,
c: syn::LitChar,
handler: Handler,
}
struct Handler(Option<Path>);
impl Parse for Symbol {
fn parse(input: ParseStream) -> Result<Self> {
let name = input.call(Ident::parse_any)?;
@ -55,9 +74,13 @@ impl Parse for Symbol {
impl Parse for Kind {
fn parse(input: ParseStream) -> Result<Self> {
let handler = input.parse::<Handler>()?;
if input.peek(syn::LitChar) {
Ok(Self::Single(input.parse()?))
Ok(Self::Single(input.parse()?, handler))
} else {
if handler.0.is_some() {
return Err(input.error("unexpected handler"));
}
let content;
syn::bracketed!(content in input);
Ok(Self::Multiple(Punctuated::parse_terminated(&content)?))
@ -68,6 +91,7 @@ impl Parse for Kind {
impl Parse for Variant {
fn parse(input: ParseStream) -> Result<Self> {
let mut name = String::new();
let handler = input.parse::<Handler>()?;
if input.peek(syn::Ident::peek_any) {
name.push_str(&input.call(Ident::parse_any)?.to_string());
while input.peek(Token![.]) {
@ -78,6 +102,26 @@ impl Parse for Variant {
input.parse::<Token![:]>()?;
}
let c = input.parse()?;
Ok(Self { name, c })
Ok(Self { name, c, handler })
}
}
impl Parse for Handler {
fn parse(input: ParseStream) -> Result<Self> {
let Ok(attrs) = input.call(syn::Attribute::parse_outer) else {
return Ok(Self(None));
};
let handler = attrs
.iter()
.find_map(|attr| {
if attr.path().is_ident("call") {
if let Ok(path) = attr.parse_args::<Path>() {
return Some(Self(Some(path)));
}
}
None
})
.unwrap_or(Self(None));
Ok(handler)
}
}

View File

@ -9,8 +9,7 @@ use crate::foundations::{
Context, Func, IntoValue, NativeElement, Scope, Scopes, Value,
};
use crate::introspection::Introspector;
use crate::math::{Accent, AccentElem, LrElem};
use crate::symbols::Symbol;
use crate::math::LrElem;
use crate::syntax::ast::{self, AstNode};
use crate::syntax::{Span, Spanned, SyntaxNode};
use crate::text::TextElem;
@ -129,23 +128,9 @@ impl Eval for ast::FuncCall<'_> {
(callee.eval(vm)?, args.eval(vm)?.spanned(span))
};
// Handle math special cases for non-functions:
// Combining accent symbols apply themselves while everything else
// simply displays the arguments verbatim.
if in_math && !matches!(callee, Value::Func(_)) {
if let Value::Symbol(sym) = &callee {
let c = sym.get();
if let Some(accent) = Symbol::combining_accent(c) {
let base = args.expect("base")?;
let size = args.named("size")?;
args.finish()?;
let mut accent = AccentElem::new(base, Accent::new(accent));
if let Some(size) = size {
accent = accent.with_size(size);
}
return Ok(Value::Content(accent.pack()));
}
}
let func_result = callee.clone().cast::<Func>().at(callee_span);
if in_math && func_result.is_err() {
// For non-functions in math, we wrap the arguments in parentheses.
let mut body = Content::empty();
for (i, arg) in args.all::<Content>()?.into_iter().enumerate() {
if i > 0 {
@ -163,11 +148,10 @@ impl Eval for ast::FuncCall<'_> {
));
}
let callee = callee.cast::<Func>().at(callee_span)?;
let point = || Tracepoint::Call(callee.name().map(Into::into));
let func = func_result?;
let point = || Tracepoint::Call(func.name().map(Into::into));
let f = || {
callee
.call(&mut vm.engine, vm.context, args)
func.call(&mut vm.engine, vm.context, args)
.trace(vm.world(), point, span)
};

View File

@ -107,7 +107,7 @@ impl Eval for ast::Escape<'_> {
type Output = Value;
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(Value::Symbol(Symbol::single(self.get())))
Ok(Value::Symbol(Symbol::single(self.get().into())))
}
}
@ -115,7 +115,7 @@ impl Eval for ast::Shorthand<'_> {
type Output = Value;
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(Value::Symbol(Symbol::single(self.get())))
Ok(Value::Symbol(Symbol::single(self.get().into())))
}
}

View File

@ -277,12 +277,12 @@ impl Repr for Selector {
cast! {
type Selector,
text: EcoString => Self::text(&text)?,
func: Func => func
.element()
.ok_or("only element functions can be used as selectors")?
.select(),
label: Label => Self::Label(label),
text: EcoString => Self::text(&text)?,
regex: Regex => Self::regex(regex)?,
location: Location => Self::Location(location),
}

View File

@ -658,7 +658,8 @@ primitive! { Dict: "dictionary", Dict }
primitive! {
Func: "function",
Func,
Type(ty) => ty.constructor()?.clone()
Type(ty) => ty.constructor()?.clone(),
Symbol(symbol) => symbol.func()?
}
primitive! { Args: "arguments", Args }
primitive! { Type: "type", Type }

View File

@ -1,18 +1,64 @@
use crate::diag::{bail, SourceResult};
use crate::foundations::{
cast, elem, Content, Packed, Resolve, Smart, StyleChain, Value,
cast, elem, func, Content, NativeElement, Packed, Resolve, Smart, StyleChain, Value,
};
use crate::layout::{Em, Frame, Length, Point, Rel, Size};
use crate::math::{
style_cramped, FrameFragment, GlyphFragment, LayoutMath, MathContext, MathFragment,
Scaled,
};
use crate::symbols::Symbol;
use crate::text::TextElem;
/// How much the accent can be shorter than the base.
const ACCENT_SHORT_FALL: Em = Em::new(0.5);
/// This macro generates accent-related functions.
///
/// ```ignore
/// accents! {
/// '\u{0300}' | '`' => grave,
/// // ^^^^^^^^^ ^^^ ^^^^^
/// // | | |
/// // | | +-- The name of the function.
/// // | +--------- The alternative characters that represent the accent.
/// // +---------------------- The primary character that represents the accent.
/// }
/// ```
///
/// When combined with the `Accent::combine` function, accent characters can be normalized
/// to the primary character.
macro_rules! accents {
($($primary:literal $(| $alt:literal)* => $name:ident),* $(,)?) => {
impl Accent {
/// Normalize an accent to a combining one.
pub fn combine(c: char) -> Option<char> {
Some(match c {
$($primary $(| $alt)* => $primary,)*
_ => return None,
})
}
}
$(
/// The accent function for callable symbol definitions.
#[func]
pub fn $name(
/// The base to which the accent is applied.
base: Content,
/// The size of the accent, relative to the width of the base.
#[named]
size: Option<Smart<Rel<Length>>>,
) -> Content {
let mut accent = AccentElem::new(base, Accent::new($primary));
if let Some(size) = size {
accent = accent.with_size(size);
}
accent.pack()
}
)+
};
}
/// Attaches an accent to a base.
///
/// # Example
@ -43,6 +89,7 @@ pub struct AccentElem {
/// | Circumflex | `hat` | `^` |
/// | Tilde | `tilde` | `~` |
/// | Macron | `macron` | `¯` |
/// | Dash | `dash` | `‾` |
/// | Breve | `breve` | `˘` |
/// | Dot | `dot` | `.` |
/// | Double dot, Diaeresis | `dot.double`, `diaer` | `¨` |
@ -130,10 +177,33 @@ pub struct Accent(char);
impl Accent {
/// Normalize a character into an accent.
pub fn new(c: char) -> Self {
Self(Symbol::combining_accent(c).unwrap_or(c))
Self(Self::combine(c).unwrap_or(c))
}
}
// Keep it synced with the documenting table above.
accents! {
'\u{0300}' | '`' => grave,
'\u{0301}' | '´' => acute,
'\u{0302}' | '^' | 'ˆ' => hat,
'\u{0303}' | '~' | '' | '˜' => tilde,
'\u{0304}' | '¯' => macron,
'\u{0305}' | '-' | '‾' | '' => dash,
'\u{0306}' | '˘' => breve,
'\u{0307}' | '.' | '˙' | '⋅' => dot,
'\u{0308}' | '¨' => dot_double,
'\u{20db}' => dot_triple,
'\u{20dc}' => dot_quad,
'\u{030a}' | '∘' | '○' => circle,
'\u{030b}' | '˝' => acute_double,
'\u{030c}' | 'ˇ' => caron,
'\u{20d6}' | '←' => arrow_l,
'\u{20d7}' | '→' | '⟶' => arrow,
'\u{20e1}' | '↔' | '⟷' => arrow_l_r,
'\u{20d0}' | '↼' => harpoon_lt,
'\u{20d1}' | '⇀' => harpoon,
}
cast! {
Accent,
self => self.0.into_value(),

View File

@ -2,7 +2,9 @@
#[macro_use]
mod ctx;
mod accent;
pub mod accent;
mod align;
mod attach;
mod cancel;
@ -21,7 +23,7 @@ mod stretch;
mod style;
mod underover;
pub use self::accent::*;
pub use self::accent::{Accent, AccentElem};
pub use self::align::*;
pub use self::attach::*;
pub use self::cancel::*;

View File

@ -84,7 +84,7 @@ pub(crate) const SYM: &[(&str, Symbol)] = symbols! {
comma: ',',
dagger: ['†', double: ''],
dash: [
en: '',
#[call(crate::math::accent::dash)] en: '',
em: '',
fig: '',
wave: '',
@ -93,15 +93,15 @@ pub(crate) const SYM: &[(&str, Symbol)] = symbols! {
wave.double: '',
],
dot: [
op: '',
#[call(crate::math::accent::dot)] op: '',
basic: '.',
c: '·',
circle: '',
circle.big: '',
square: '',
double: '¨',
triple: '\u{20db}',
quad: '\u{20dc}',
#[call(crate::math::accent::dot_double)] double: '¨',
#[call(crate::math::accent::dot_triple)] triple: '\u{20db}',
#[call(crate::math::accent::dot_quad)] quad: '\u{20dc}',
],
excl: ['!', double: '', inv: '¡', quest: ''],
quest: ['?', double: '', excl: '', inv: '¿'],
@ -117,7 +117,7 @@ pub(crate) const SYM: &[(&str, Symbol)] = symbols! {
slash: ['/', double: '', triple: '', big: '\u{29f8}'],
dots: [h.c: '', h: '', v: '', down: '', up: ''],
tilde: [
op: '',
#[call(crate::math::accent::tilde)] op: '',
basic: '~',
dot: '',
eq: '',
@ -133,14 +133,17 @@ pub(crate) const SYM: &[(&str, Symbol)] = symbols! {
],
// Accents, quotes, and primes.
acute: ['´', double: '˝'],
breve: '˘',
acute: [
#[call(crate::math::accent::acute)] '´',
#[call(crate::math::accent::acute_double)] double: '˝',
],
breve: #[call(crate::math::accent::breve)] '˘',
caret: '',
caron: 'ˇ',
hat: '^',
diaer: '¨',
grave: '`',
macron: '¯',
caron: #[call(crate::math::accent::caron)] ',
hat: #[call(crate::math::accent::hat)] '^',
diaer: #[call(crate::math::accent::dot_double)] ',
grave: #[call(crate::math::accent::grave)] '`',
macron: #[call(crate::math::accent::macron)] ',
quote: [
double: '"',
single: '\'',
@ -487,7 +490,7 @@ pub(crate) const SYM: &[(&str, Symbol)] = symbols! {
// Shapes.
bullet: '',
circle: [
stroked: '',
#[call(crate::math::accent::circle)] stroked: '',
stroked.tiny: '',
stroked.small: '',
stroked.big: '',
@ -580,7 +583,7 @@ pub(crate) const SYM: &[(&str, Symbol)] = symbols! {
// Arrows, harpoons, and tacks.
arrow: [
r: '',
#[call(crate::math::accent::arrow)] r: '',
r.long.bar: '',
r.bar: '',
r.curve: '',
@ -607,7 +610,7 @@ pub(crate) const SYM: &[(&str, Symbol)] = symbols! {
r.twohead.bar: '',
r.twohead: '',
r.wave: '',
l: '',
#[call(crate::math::accent::arrow_l)] l: '',
l.bar: '',
l.curve: '',
l.dashed: '',
@ -656,7 +659,7 @@ pub(crate) const SYM: &[(&str, Symbol)] = symbols! {
b.stroked: '',
b.triple: '',
b.twohead: '',
l.r: '',
#[call(crate::math::accent::arrow_l_r)] l.r: '',
l.r.double: '',
l.r.double.long: '',
l.r.double.not: '',
@ -715,13 +718,13 @@ pub(crate) const SYM: &[(&str, Symbol)] = symbols! {
b: '',
],
harpoon: [
rt: '',
#[call(crate::math::accent::harpoon)] rt: '',
rt.bar: '',
rt.stop: '',
rb: '',
rb.bar: '',
rb.stop: '',
lt: '',
#[call(crate::math::accent::harpoon_lt)] lt: '',
lt.bar: '',
lt.stop: '',
lb: '',

View File

@ -7,7 +7,7 @@ use ecow::{eco_format, EcoString};
use serde::{Serialize, Serializer};
use crate::diag::{bail, SourceResult, StrResult};
use crate::foundations::{cast, func, scope, ty, Array};
use crate::foundations::{cast, func, scope, ty, Array, Func};
use crate::syntax::{Span, Spanned};
#[doc(inline)]
@ -46,43 +46,52 @@ pub use typst_macros::symbols;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Symbol(Repr);
/// The character of a symbol, possibly with a function.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct SymChar(char, Option<fn() -> Func>);
/// The internal representation.
#[derive(Clone, Eq, PartialEq, Hash)]
enum Repr {
Single(char),
Const(&'static [(&'static str, char)]),
Single(SymChar),
Const(&'static [(&'static str, SymChar)]),
Multi(Arc<(List, EcoString)>),
}
/// A collection of symbols.
#[derive(Clone, Eq, PartialEq, Hash)]
enum List {
Static(&'static [(&'static str, char)]),
Runtime(Box<[(EcoString, char)]>),
Static(&'static [(&'static str, SymChar)]),
Runtime(Box<[(EcoString, SymChar)]>),
}
impl Symbol {
/// Create a new symbol from a single character.
pub const fn single(c: char) -> Self {
pub const fn single(c: SymChar) -> Self {
Self(Repr::Single(c))
}
/// Create a symbol with a static variant list.
#[track_caller]
pub const fn list(list: &'static [(&'static str, char)]) -> Self {
pub const fn list(list: &'static [(&'static str, SymChar)]) -> Self {
debug_assert!(!list.is_empty());
Self(Repr::Const(list))
}
/// Create a symbol with a runtime variant list.
#[track_caller]
pub fn runtime(list: Box<[(EcoString, char)]>) -> Self {
pub fn runtime(list: Box<[(EcoString, SymChar)]>) -> Self {
debug_assert!(!list.is_empty());
Self(Repr::Multi(Arc::new((List::Runtime(list), EcoString::new()))))
}
/// Get the symbol's text.
/// Get the symbol's char.
pub fn get(&self) -> char {
self.sym().char()
}
/// Resolve the symbol's `SymChar`.
pub fn sym(&self) -> SymChar {
match &self.0 {
Repr::Single(c) => *c,
Repr::Const(_) => find(self.variants(), "").unwrap(),
@ -90,6 +99,13 @@ impl Symbol {
}
}
/// Try to get the function associated with the symbol, if any.
pub fn func(&self) -> StrResult<Func> {
self.sym()
.func()
.ok_or_else(|| eco_format!("symbol {self} is not callable"))
}
/// Apply a modifier to the symbol.
pub fn modified(mut self, modifier: &str) -> StrResult<Self> {
if let Repr::Const(list) = self.0 {
@ -111,7 +127,7 @@ impl Symbol {
}
/// The characters that are covered by this symbol.
pub fn variants(&self) -> impl Iterator<Item = (&str, char)> {
pub fn variants(&self) -> impl Iterator<Item = (&str, SymChar)> {
match &self.0 {
Repr::Single(c) => Variants::Single(Some(*c).into_iter()),
Repr::Const(list) => Variants::Static(list.iter()),
@ -133,33 +149,6 @@ impl Symbol {
}
set.into_iter()
}
/// Normalize an accent to a combining one. Keep it synced with the
/// documenting table in accent.rs AccentElem.
pub fn combining_accent(c: char) -> Option<char> {
Some(match c {
'\u{0300}' | '`' => '\u{0300}',
'\u{0301}' | '´' => '\u{0301}',
'\u{0302}' | '^' | 'ˆ' => '\u{0302}',
'\u{0303}' | '~' | '' | '˜' => '\u{0303}',
'\u{0304}' | '¯' => '\u{0304}',
'\u{0305}' | '-' | '‾' | '' => '\u{0305}',
'\u{0306}' | '˘' => '\u{0306}',
'\u{0307}' | '.' | '˙' | '⋅' => '\u{0307}',
'\u{0308}' | '¨' => '\u{0308}',
'\u{20db}' => '\u{20db}',
'\u{20dc}' => '\u{20dc}',
'\u{030a}' | '∘' | '○' => '\u{030a}',
'\u{030b}' | '˝' => '\u{030b}',
'\u{030c}' | 'ˇ' => '\u{030c}',
'\u{20d6}' | '←' => '\u{20d6}',
'\u{20d7}' | '→' | '⟶' => '\u{20d7}',
'\u{20e1}' | '↔' | '⟷' => '\u{20e1}',
'\u{20d0}' | '↼' => '\u{20d0}',
'\u{20d1}' | '⇀' => '\u{20d1}',
_ => return None,
})
}
}
#[scope]
@ -203,7 +192,7 @@ impl Symbol {
if list.iter().any(|(prev, _)| &v.0 == prev) {
bail!(span, "duplicate variant");
}
list.push((v.0, v.1));
list.push((v.0, SymChar::pure(v.1)));
}
Ok(Symbol::runtime(list.into_boxed_slice()))
}
@ -215,6 +204,34 @@ impl Display for Symbol {
}
}
impl SymChar {
/// Create a symbol character without a function.
pub const fn pure(c: char) -> Self {
Self(c, None)
}
/// Create a symbol character with a function.
pub const fn with_func(c: char, func: fn() -> Func) -> Self {
Self(c, Some(func))
}
/// Get the character of the symbol.
pub const fn char(&self) -> char {
self.0
}
/// Get the function associated with the symbol.
pub fn func(&self) -> Option<Func> {
self.1.map(|f| f())
}
}
impl From<char> for SymChar {
fn from(c: char) -> Self {
SymChar(c, None)
}
}
impl Debug for Repr {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
@ -276,13 +293,13 @@ cast! {
/// Iterator over variants.
enum Variants<'a> {
Single(std::option::IntoIter<char>),
Static(std::slice::Iter<'static, (&'static str, char)>),
Runtime(std::slice::Iter<'a, (EcoString, char)>),
Single(std::option::IntoIter<SymChar>),
Static(std::slice::Iter<'static, (&'static str, SymChar)>),
Runtime(std::slice::Iter<'a, (EcoString, SymChar)>),
}
impl<'a> Iterator for Variants<'a> {
type Item = (&'a str, char);
type Item = (&'a str, SymChar);
fn next(&mut self) -> Option<Self::Item> {
match self {
@ -295,9 +312,9 @@ impl<'a> Iterator for Variants<'a> {
/// Find the best symbol from the list.
fn find<'a>(
variants: impl Iterator<Item = (&'a str, char)>,
variants: impl Iterator<Item = (&'a str, SymChar)>,
modifiers: &str,
) -> Option<char> {
) -> Option<SymChar> {
let mut best = None;
let mut best_score = None;

View File

@ -661,15 +661,15 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel {
for (variant, c) in symbol.variants() {
let shorthand = |list: &[(&'static str, char)]| {
list.iter().copied().find(|&(_, x)| x == c).map(|(s, _)| s)
list.iter().copied().find(|&(_, x)| x == c.char()).map(|(s, _)| s)
};
list.push(SymbolModel {
name: complete(variant),
markup_shorthand: shorthand(typst::syntax::ast::Shorthand::MARKUP_LIST),
math_shorthand: shorthand(typst::syntax::ast::Shorthand::MATH_LIST),
codepoint: c as u32,
accent: typst::symbols::Symbol::combining_accent(c).is_some(),
codepoint: c.char() as _,
accent: typst::math::Accent::combine(c.char()).is_some(),
alternates: symbol
.variants()
.filter(|(other, _)| other != &variant)