mawww / kakoune

mawww's experiment for a better code editor
http://kakoune.org
The Unlicense
9.96k stars 715 forks source link

Catch blocks should allow matching on the error message #2889

Open Screwtapello opened 5 years ago

Screwtapello commented 5 years ago

Currently, Kakoune commands can fail at runtime, either deliberately with the :fail command or when something unexpected happens, like when the <a-k> ("keep matching") command is given a regex that doesn't match any selection.

Kakoune-script provides a way to handle such errors with the :try command. An error causes control flow to jump out of the try block and into the following catch block (if any); an error in the catch block causes control flow to jump into the catch block after that, etc. This allows simple scripts to produce sophisticated behaviour, like the Use Tab for both indenting and completion snippet which uses :execute-keys inside :try to determine whether or not to set up mappings.

Unfortunately, as in other programming languages, silently eating errors makes problems more difficult to debug than they should be. The :execute-keys command in that snippet is supposed to fail only when all cursors are preceded by whitespace, however it's easy to imagine a slight variant of the command that would fail due to a typo in the regex, or for some other trivial reason. Since :try eats all errors, the snippet would silently do nothing, making it very difficult to debug.

It is possible to have :try eat some errors but not others. Inside each catch block, the %val{error} expansion is set to the text of the error message, so you can do something like:

try %{
    execute-keys -draft 'h<a-K>\h<ret>'
    map window insert <tab> <c-n>
    map window insert <s-tab> <c-p>

} catch {
    eval %sh{
        case "$kak_error" in
            no selections remaining)
                # we expected this, it's fine
                printf "nop\n"
                ;;
            *)
                # something unexpected occurred,
                # re-raise the same error
                printf "fail %s\n" "$kak_error"
                ;;
        esac
    }
}

This works, but:

To fix this, I think the catch clause should work a little more like the :hook command, having a regex parameter:

try %{
    execute-keys -draft 'h<a-K>\h<ret>'
    map window insert <tab> <c-n>
    map window insert <s-tab> <c-p>

} catch no\sselections\sremaining {
    # we expected this, it's fine
    nop
}

When Kakoune is handling an error and reaches a catch block, it should match the error text against the block's regex, and only execute the block if a match is found. Like :hook, the regex must match the entire string, as if it were wrapped in ^$. If the error text does not match the regex, Kakoune should keep looking for the next catch block, hopefully with the original error line/column information. The %val{error} expansion would still be available, just like %val{hook_param}.

One downside of this approach is that it would be incompatible with existing code that uses catch. If that's a concern, perhaps a different keyword could be used, like except, or it could be an optional flag, like catch -matching some.*regex

Another downside is that makes Kakoune-script slightly closer to being a Real Language, instead of handling all control-flow in POSIX shell as intended. I think this is a reasonable expansion, since :try already exists, but I could be convinced the other way.

Screwtapello commented 5 years ago

@occivink on IRC mentioned an example like this:

try %{
    fail error1
} catch error1 %{
    fail error2
} catch error2 %{
    echo -debug "???"
}

Would the above code echo ??? to the debug buffer?

If you translate the above code to Kakoune's existing syntax, then yes: each catch block handles errors from the original try block and all previous catch blocks. This makes it easy to write code that tries a number of alternatives in order, without ridiculous nesting.

If you translate the above code to some other language with exceptions, like C++ or Python, then no: all the catch blocks only handle errors thrown by the original try block, not by any of the other catch blocks. This makes it easy to write code that robustly handles specific errors, because you can be sure where a particular error came from.

On one hand, robust error handling is important for reliable, robust software, so I'm sympathetic to the idea that Kakoune should follow the model of other languages with exception handling.

On the other hand, literally no scripts in Kakoune's standard library check for specific errors at all today, so there doesn't appear to be a screaming need for rigorousness. Also, changing the exception-handling model that much would make migration from Kakoune's current syntax much more difficult.

Considering both options, I think it would be best to keep Kakoune's existing exception-handling model, and therefore I propose the example at the beginning of this comment should echo ??? to the debug buffer.

andreyorst commented 5 years ago

I agree with the proposal to make try blocks more strict, and I think that try blocks that doesn't have any catch should be replaced with ignore_errors block, while making try catch more flexible solution for error handling, meaning that any try must have at least onecatch .* block.

ignore_errors %{
    delete-buffer vaiv
}

Also I'd like to propose final for handling errors that didn't matched any catch:,

try %{
    evaluate-command vaiv
} catch .*no\ssuch\scommand.* %{
    ...
} catch "some vaiv error"  %{
    ...
} final %{
   echo -debug "can't handle error"
}

however this would be a huge change, and to slim it down, catch could take an optional argument, which means if no pattern specified, act like final. This way it won't require any change to existing scripts.

This also involves the addition of throw command, that will act like fail but with a proper name

FlyingWombat commented 5 years ago

I'd like to propose final for handling errors that didn't match any catch

IMO, final, should be a clause that always executes, as it is in python. But I hardly see a need for that in Kakoune.
I think catching any error should just be catch %{...}, and ignoring all errors should continue to just be try %{...} with no catch clause, or try %{...} catch %{ nop } if we want to make it more explicit.


I'd also like to point out that this feature would benefit from some form of standardization for error messages, to avoid making scripts more brittle.
Two solutions, off the top of my head, are:


literally no scripts in Kakoune's standard library check for specific errors at all today, so there doesn't appear to be a screaming need for rigorousness.

This is a sort of "innovator's dilemma". You are right that it means there isn't a pressing need. But, would we use the feature if it existed? Is no one checking for specific errors because it's not helpful? Or is it because the only current method is an awkward hack?