-
-
Notifications
You must be signed in to change notification settings - Fork 11
Implement optional modifiers #105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
This improves type safety to prevent bugs like the one also fixed here (best_match_in still contained a wrong call to `contains`)
This looks great! The list shows how useful this is to avoid ambiguous cases that would easily break in the future. For the globe it is a bit unfortunate as it makes sense to write Maybe a solution would be to define
and have the validation code recognize that |
Yes, that's what I meant with "duplicating the variant". Tho your example wouldn't work since
The issue with this is something that ties into the next thing I want to talk about here (see my comment below this). |
These changes to Now my point here is that I actually think exposing this is a good thing and we should give typst users access to the new resolution system too, so custom symbols aren't second-class citizens. In codex, we can just run the test
|
Another small thing that confused me a bit (and why I had to double-check that |
I think a fast implementation in Typst should be possible. To detect overlaps it should be sufficient to populate a dictionary where the keys are all "aliases" of all variants. The initial list of keys could be generated statically when building the Typst executable. Then checking overlap for a new symbol would just be a dict insertion for each new alias (negligible runtime) which would fail if the key already exists (unless I'm missing something and the check must be more sophisticated?). |
Keeping a dict might be fine in practice, but it does have exponential space complexity in the worst case ( |
If that's a concern, what about an algorithm that dynamically generates a dict for the symbol under consideration? I.e. when the user wants to define |
That's already what I meant. But I suppose the memory usage isn't actually that bad for all sane applications... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First and foremost, I'm sorry for taking so much time to consider this PR. I think the concept of optional should be implemented at some point, but I did not find the time to review your implementation until now.
This is also a breaking change since it changes the way
ModifierSet
works, so I should probably remember to bump the version inCargo.toml
before we merge this.
The public interface was already changed multiple times in already merged PRs after the latest release. Laurenz will take care of the version bump when making a new release.
I'm wondering whether it would be better to first implement optional modifiers, keeping all modifiers optional, and only then make a PR where we decide which modifiers become non-optional. I did not review changes to the symbol lists for now.
/// 1. Number of modifiers in common with `self` (more is better). | ||
/// 2. Total number of modifiers (fewer is better). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// 1. Number of modifiers in common with `self` (more is better). | |
/// 2. Total number of modifiers (fewer is better). | |
/// 1. Number of optional modifiers in common with `self` (more is better). | |
/// 2. Total number of optional modifiers (fewer is better). |
I'm wondering whether this would be easier to understand?
src/lib.rs
Outdated
if *last < max_index { | ||
*last += 1; | ||
} else { | ||
next_subseq(left, max_index - 1)?; | ||
*last = left.last().copied().map_or(*last, |x| x + 1); | ||
} | ||
Some(()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if *last < max_index { | |
*last += 1; | |
} else { | |
next_subseq(left, max_index - 1)?; | |
*last = left.last().copied().map_or(*last, |x| x + 1); | |
} | |
Some(()) | |
if *last < max_index { | |
*last += 1; | |
} else { | |
*last = next_subseq(left, max_index - 1)?; | |
} | |
Some(*last) |
This seems more idiomatic to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree. next_subseq
shouldn't return anything since the "return value" is already its first argument. Adding a second return value makes the API harder to understand IMO.
let mset = indices.iter().map(|i| modifs[*i]).fold( | ||
ModifierSet::<String>::default(), | ||
|mut res, m| { | ||
res.insert_raw(m); | ||
res | ||
}, | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should have an impl<S: Deref<Target = str>> FromIterator<S> for ModifierSet<String>
instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really don't like that, see #46 (comment).
Maybe we could have an inherent method from_iter_raw
that documents all of those requirements (S::default()
must be empty, each item must be a single modifier). To use that like collect
, we'd also need an iterator extension trait, which seems overkill, but maybe calling it directly on an iterator is already ergonomic enough?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh indeed you are right! I had forgotten I already suggested a similar thing before.
I think having from_iter_raw
could be useful.
Co-authored-by: Malo <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't review the code too closely, but I'm okay with this in principle. I just noticed one thing while going through it. Otherwise, I'd defer to @MDLC01's judgement.
@@ -123,9 +158,44 @@ impl<S: Default> Default for ModifierSet<S> { | |||
} | |||
} | |||
|
|||
#[derive(Copy, Clone)] | |||
pub struct Modifier<'a>(&'a str); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This type appears in the public API, but is not re-exported, so it's a deadlink in the docs. If it's supposed to properly public, it would also be good to have a bit of docs on it.
Closes #51.
This is mostly done, but there are some unresolved questions and some issues and it still needs some finishing touches like documentation, which is why it's a draft for now.
This is also a breaking change since it changes the way
ModifierSet
works, so I should probably remember to bump the version inCargo.toml
before we merge this.Changes to observable syntax
The way I've implemented it, the
modifier?
syntax applies directly in theModifierSet
, so it would be exposed to the users in typst too. (Or typst would have to do some extra sanitizing).And also, it's not really clear how this whole new resolution algorithm would interact with user-defined symbols from typst: I've kept the code in
best_match_in
mostly the same for now, but we could strip it down way more if the match was actually guaranteed to be unique. (But of course, this may not be true for user-defined symbols.)Changes to variant resolution
I have another branch where I wrote some ad-hoc code (mostly identical to what is now the
no_overlap
test) to brute-force check every possible modifier set for every symbol and generate a list of which ones differ between the old algorithm and the new one.This is that list:
emoji.globe
where a variant has two optional modifiers, of which at least one needs to be present since it is not the default variant. This could in theory be encoded by duplicating the variant, but that'd be observable in the repr and also a bit hacky, so I haven't done it for now.Other stuff
I have some other minor things I'd like to say, but I'm too tired now, so I'll add them later.