Skip to content
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

Replace direct panics with Errors #432

Merged
merged 1 commit into from Apr 28, 2022
Merged

Conversation

Enselic
Copy link
Collaborator

@Enselic Enselic commented Mar 27, 2022

Even though there are still .unwrap()s and .expect()s left, this change should cover the most common error code paths.

If we find a new case of an error that we want to propagate, it is generally straightforward to deprecate the old function and add a new function that calls the old function and .unwrap(). This can be done in a minor release. In many cases this can hopefully be done without API changes since many parts of the API have been changed already.

On my low-end desktop, this PR does seem to have a slight (1-2%-ish) impact on performance. But it is hard to make a final judgement, because I get that kind of performance difference between benchmarks of the same commit on master. Either way, I think it is reasonable and expected to pay a slight performance penalty for (checking for) error propagation from hot code paths. But I am open to hearing other opinions.

Here is an example run of cargo bench with master as baseline (click to expand):

cargo bench -- --baseline master-6b211f9-4
/home/martin/src/syntect
46882e1 Replace direct panics with Errors  dont-panic   (origin/dont-panic)
% cargo bench -- --baseline master-6b211f9-4 
   Compiling syntect v4.7.1 (/home/martin/src/syntect)
    Finished bench [optimized] target(s) in 50.47s
     Running unittests (target/release/deps/highlighting-796b26013708fde3)
stack_matching          time:   [84.617 ns 84.639 ns 84.665 ns]                          
                        change: [-0.3473% -0.0122% +0.3243%] (p = 0.94 > 0.05)
                        No change in performance detected.
Found 1 outliers among 10 measurements (10.00%)
  1 (10.00%) high severe

highlight_html          time:   [456.56 ms 457.73 ms 459.21 ms]                         
                        change: [+1.3842% +1.7863% +2.1887%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 2 outliers among 10 measurements (20.00%)
  1 (10.00%) high mild
  1 (10.00%) high severe

highlight/"highlight_test.erb"                                                                            
                        time:   [1.8383 ms 1.8521 ms 1.8851 ms]
                        change: [-4.6083% +2.5368% +10.0000%] (p = 0.55 > 0.05)
                        No change in performance detected.
Found 1 outliers among 10 measurements (10.00%)
  1 (10.00%) high severe
highlight/"InspiredGitHub.tmTheme"                                                                           
                        time:   [25.995 ms 26.122 ms 26.244 ms]
                        change: [-0.2425% +0.7742% +1.6733%] (p = 0.15 > 0.05)
                        No change in performance detected.
Found 1 outliers among 10 measurements (10.00%)
  1 (10.00%) high mild
highlight/"Ruby.sublime-syntax"                                                                           
                        time:   [79.368 ms 79.514 ms 79.615 ms]
                        change: [+0.7164% +1.1379% +1.5952%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 1 outliers among 10 measurements (10.00%)
  1 (10.00%) high severe
Benchmarking highlight/"jquery.js": Warming up for 3.0000 s
Warning: Unable to complete 10 samples in 5.0s. You may wish to increase target time to 6.6s.
highlight/"jquery.js"   time:   [669.55 ms 670.36 ms 671.18 ms]                                
                        change: [+0.5865% +1.3819% +2.0224%] (p = 0.00 < 0.05)
                        Change within noise threshold.
highlight/"parser.rs"   time:   [442.50 ms 443.91 ms 445.63 ms]                                
                        change: [+1.3417% +1.7942% +2.2518%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 2 outliers among 10 measurements (20.00%)
  1 (10.00%) high mild
  1 (10.00%) high severe
highlight/"scope.rs"    time:   [43.036 ms 43.139 ms 43.304 ms]                                
                        change: [+1.6237% +3.4113% +5.2355%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 2 outliers among 10 measurements (20.00%)
  2 (20.00%) high severe

     Running unittests (target/release/deps/load_and_highlight-ce9210706bcf7470)
load_and_highlight/"highlight_test.erb"                                                                           
                        time:   [32.574 ms 32.603 ms 32.638 ms]
                        change: [+0.0331% +0.1842% +0.3482%] (p = 0.02 < 0.05)
                        Change within noise threshold.
Found 4 outliers among 50 measurements (8.00%)
  2 (4.00%) high mild
  2 (4.00%) high severe
load_and_highlight/"InspiredGitHub.tmTheme"                                                                           
                        time:   [30.666 ms 30.724 ms 30.785 ms]
                        change: [-0.9729% -0.7384% -0.5029%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 3 outliers among 50 measurements (6.00%)
  3 (6.00%) high mild
load_and_highlight/"Ruby.sublime-syntax"                                                                           
                        time:   [88.468 ms 88.529 ms 88.595 ms]
                        change: [+4.5497% +4.6433% +4.7461%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 2 outliers among 50 measurements (4.00%)
  1 (2.00%) high mild
  1 (2.00%) high severe
Benchmarking load_and_highlight/"parser.rs": Warming up for 3.0000 s
Warning: Unable to complete 50 samples in 5.0s. You may wish to increase target time to 22.8s, or reduce sample count to 10.
load_and_highlight/"parser.rs"                                                                          
                        time:   [453.57 ms 454.30 ms 455.10 ms]
                        change: [+1.7068% +1.9070% +2.1127%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 7 outliers among 50 measurements (14.00%)
  7 (14.00%) high mild

     Running unittests (target/release/deps/loading-e67ac9fa9c16eca0)
load_internal_dump      time:   [1.5755 ms 1.5766 ms 1.5780 ms]                               
                        change: [+23.243% +23.647% +24.054%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 4 outliers among 50 measurements (8.00%)
  4 (8.00%) high severe

load_internal_themes    time:   [1.6210 ms 1.6215 ms 1.6222 ms]                                 
                        change: [+1.7412% +2.0074% +2.2935%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 6 outliers among 50 measurements (12.00%)
  2 (4.00%) high mild
  4 (8.00%) high severe

load_theme              time:   [1.9683 ms 1.9690 ms 1.9698 ms]                       
                        change: [+0.1474% +0.4301% +0.6828%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 5 outliers among 50 measurements (10.00%)
  1 (2.00%) high mild
  4 (8.00%) high severe

Benchmarking add_from_folder: Warming up for 3.0000 s
Warning: Unable to complete 50 samples in 5.0s. You may wish to increase target time to 28.1s, or reduce sample count to 10.
add_from_folder         time:   [561.55 ms 561.74 ms 561.94 ms]                          
                        change: [-0.4230% -0.3721% -0.3231%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 2 outliers among 50 measurements (4.00%)
  2 (4.00%) high severe

Benchmarking link_syntaxes: Warming up for 3.0000 s
Warning: Unable to complete 50 samples in 5.0s. You may wish to increase target time to 9.8s, or reduce sample count to 20.
link_syntaxes           time:   [195.43 ms 195.55 ms 195.68 ms]                        
                        change: [-0.3678% -0.2794% -0.1981%] (p = 0.00 < 0.05)
                        Change within noise threshold.

from_dump_file          time:   [1.3722 ms 1.3745 ms 1.3765 ms]                           
                        change: [-8.2878% -7.0064% -5.9791%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 3 outliers among 50 measurements (6.00%)
  1 (2.00%) high mild
  2 (4.00%) high severe

     Running unittests (target/release/deps/parsing-253403872c2ab027)
parse/"highlight_test.erb"                                                                            
                        time:   [1.8169 ms 1.8404 ms 1.8745 ms]
                        change: [-19.620% +0.6596% +29.851%] (p = 0.96 > 0.05)
                        No change in performance detected.
Found 6 outliers among 50 measurements (12.00%)
  2 (4.00%) high mild
  4 (8.00%) high severe
parse/"InspiredGitHub.tmTheme"                                                                           
                        time:   [18.250 ms 18.296 ms 18.344 ms]
                        change: [-0.8843% -0.5214% -0.1779%] (p = 0.01 < 0.05)
                        Change within noise threshold.
Found 2 outliers among 50 measurements (4.00%)
  2 (4.00%) high mild
parse/"Ruby.sublime-syntax"                                                                           
                        time:   [77.483 ms 77.529 ms 77.579 ms]
                        change: [-1.6981% -1.5932% -1.4946%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 2 outliers among 50 measurements (4.00%)
  2 (4.00%) high mild
Benchmarking parse/"jquery.js": Warming up for 30.000 s
Warning: Unable to complete 50 samples in 5.0s. You may wish to increase target time to 30.4s, or reduce sample count to 10.
parse/"jquery.js"       time:   [616.39 ms 617.68 ms 619.19 ms]                            
                        change: [+1.0887% +1.3171% +1.5799%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 5 outliers among 50 measurements (10.00%)
  5 (10.00%) high severe
Benchmarking parse/"parser.rs": Warming up for 30.000 s
Warning: Unable to complete 50 samples in 5.0s. You may wish to increase target time to 20.1s, or reduce sample count to 10.
parse/"parser.rs"       time:   [410.44 ms 410.72 ms 411.06 ms]                            
                        change: [+1.2844% +1.3908% +1.5154%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 2 outliers among 50 measurements (4.00%)
  1 (2.00%) high mild
  1 (2.00%) high severe
parse/"scope.rs"        time:   [41.482 ms 41.521 ms 41.563 ms]                            
                        change: [+2.8638% +3.0041% +3.1593%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 3 outliers among 50 measurements (6.00%)
  1 (2.00%) low mild
  1 (2.00%) high mild
  1 (2.00%) high severe

Some words on the details of this PR:

  • In examples/ I simply .unwrap()
  • In tests I do .expect("#[cfg(test)]") to make the diff easy to review
  • In Iterator for RangedHighlightIterator I do .ok()? instead of propagating the error, but I feel like propagating the error is too big of a change at this point.
  • I introduced a new ScopeError enum due to the parsing feature and the current structure of the code. It is possible that a new big restructuring of the code could avoid the need for that, but honestly I think we need to try to get 5.0.0 out at this point.
  • Inside of .any() and .map() I do .unwrap() to not have to refactor the code. That can be fixed in 5.0.1 or 5.1.0 if necessary.
  • I allow one instance of a panic!() since it seems to be too much of an edge-case to propagate. I added a comment in the code.

This PR is for #98.

FWIW, here is a CI run of bat with all regression tests passing when using the code in this PR: Enselic/bat#85

Even though there are still `.unwrap()`s and `.expects()` left, this
change should cover the most common error code paths.

If we find a new case of an error that we want to propagate, it is
generally straightforward to deprecate the old function and add a new
function that the old function calls and `.unwrap()`.
@Enselic
Copy link
Collaborator Author

Enselic commented Mar 27, 2022

I almost forgot to mention; here is how this PR impacts the syntect API compared to master:

% cargo install cargo-public-api
% cargo public-api --diff-git-checkouts origin/master dont-panic                                                                                     
Removed from the public API:
============================
(nothing)

Changes to the public API:
==========================
-pub fn syntect::parsing::syntax_definition::ContextReference::id(&self) -> ContextId
+pub fn syntect::parsing::syntax_definition::ContextReference::id(&self) -> Result<ContextId, ParsingError>
-pub fn syntect::parsing::ScopeStack::apply_with_hook<F>(&mut self, op: &ScopeStackOp, hook: F) where F: FnMut(BasicScopeStackOp, &[Scope])
+pub fn syntect::parsing::ScopeStack::apply_with_hook<F>(&mut self, op: &ScopeStackOp, hook: F) -> Result<(), ScopeError> where F: FnMut(BasicScopeStackOp, &[Scope])
-pub fn syntect::parsing::syntax_definition::Context::match_at(&self, index: usize) -> &MatchPattern
+pub fn syntect::parsing::syntax_definition::Context::match_at(&self, index: usize) -> Result<&MatchPattern, ParsingError>
-pub fn syntect::parsing::syntax_definition::ContextReference::resolve<'a>(&self, syntax_set: &'a SyntaxSet) -> &'a Context
+pub fn syntect::parsing::syntax_definition::ContextReference::resolve<'a>(&self, syntax_set: &'a SyntaxSet) -> Result<&'a Context, ParsingError>
-pub fn syntect::html::line_tokens_to_classed_spans(line: &str, ops: &[(usize, ScopeStackOp)], style: ClassStyle, stack: &mut ScopeStack) -> (String, isize)
+pub fn syntect::html::line_tokens_to_classed_spans(line: &str, ops: &[(usize, ScopeStackOp)], style: ClassStyle, stack: &mut ScopeStack) -> Result<(String, isize), Error>
-pub fn syntect::parsing::ScopeStack::apply(&mut self, op: &ScopeStackOp)
+pub fn syntect::parsing::ScopeStack::apply(&mut self, op: &ScopeStackOp) -> Result<(), ScopeError>
-pub fn syntect::html::css_for_theme_with_class_style(theme: &Theme, style: ClassStyle) -> String
+pub fn syntect::html::css_for_theme_with_class_style(theme: &Theme, style: ClassStyle) -> Result<String, Error>

Added to the public API:
========================
+pub enum syntect::parsing::ScopeError
+pub enum variant syntect::Error::ScopeError(crate::parsing::ScopeError)
+pub enum variant syntect::parsing::ParsingError::BadMatchIndex(usize)
+pub enum variant syntect::parsing::ParsingError::MissingMainContext
+pub enum variant syntect::parsing::ParsingError::UnresolvedContextReference(ContextReference)
+pub enum variant syntect::parsing::ScopeError::NoClearedScopesToRestore
+pub fn syntect::Error::from(source: crate::parsing::ScopeError) -> Self
+pub fn syntect::parsing::ScopeError::fmt(&self, __formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
+pub fn syntect::parsing::ScopeError::fmt(&self, f: &mut $crate::fmt::Formatter<'_>) -> $crate::fmt::Result

@Enselic
Copy link
Collaborator Author

Enselic commented Mar 28, 2022

To give one example where this PR helps: bat will not panic anymore when encountering sharkdp/bat#915. Instead, an error will be propagated. So users will get

[bat error]: Parsing error: Tried to use a ContextReference that has not bee resolved yet: ByScope { scope: <text.pug>, sub_context: None, with_escape: true }

instead of

thread 'main' panicked at 'Can only call resolve on linked references: ByScope { scope: <text.pug>, sub_context: None }', /Users/martin/.cargo/registry/src/github.com-1ecc6299db9ec823/syntect-4.6.0/src/parsing/syntax_definition.rs:191:18
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

(The above example is with bat end-users, but the point is of course that an error is propagated from syntect now. Also note that I ran the test with #427 reverted since otherwise no error occurs)

@Canop
Copy link
Contributor

Canop commented Mar 28, 2022

Another example is broot.
Broot uses syntect for previewing code files, and has other previewing modes.
Right now I have to use a fork of syntect with a (dirty) change to return some error rather than panicking. When there's an error, broot displays the text with another viewer without style. This is infinitely better than crashing.

Enselic added a commit to Enselic/syntect that referenced this pull request Apr 3, 2022
Update CHANGELOG.md to include the changes made in

  Replace direct panics with Errors trishume#432
@Enselic
Copy link
Collaborator Author

Enselic commented Apr 21, 2022

@trishume Hello! Just a heads-up that this is close to being in review for a month, so my intention is to merge this a week from now or so. Let me know if you want more time to review, that would be no problem at all. I am comfortable to merge this without review though, because I see it as a natural and straightforward continuation of previous PRs of the same type.

If this is merged, #409 can also be merged as is, because it has been prepared to include this PR in the 5.0.0 release notes. And once that is merged, we can release master as 5.0.0 as far as I know. See #409 (comment). Since this is the first release in the 5.x.x series, I don't think much can go wrong this time 🤞

@trishume
Copy link
Owner

Yup thanks for the heads up! I've been extra busy lately and indeed may not get to this by then, if not go ahead and merge and don't wait more for me. Sorry!

@Enselic Enselic merged commit a47dbdf into trishume:master Apr 28, 2022
@Enselic Enselic deleted the dont-panic branch April 28, 2022 18:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants