chuigda-wasteland / doc47

Documentation for the Pr47 language
7 stars 0 forks source link

P0002: Structural & checked exception for Pr47 #2

Open chuigda opened 3 years ago

chuigda commented 3 years ago

Abstract

This proposal serves to add a Java-like structural and checked exception system to the Pr47 language, in order to enhance the error handling of this language.


Existing problems and workarounds

Handling error has been a problem of long history. Currently, it is able to handle error with return values and error codes. That looks like:

func may_cause_error(args) int64 {
    if (some_condition) {
        return -1;
    }
    do_something(args);
    return 0;
}

var result int64 = may_cause_error(xxx);
if (result != 0) {
    handle_error(result);
}

This way is too primitive to be the error handling mechanism of a modern scripting language. As everyone sees, golang has been criticized for so long for such error handling feature. What's more, Pr47 is intended to support seeming interaction with Rust, while most Rustaceans do not handle errors with error codes -- They use the Result<T, E> type.


This proposal's method

By adding structural, checked exceptions into our language, allowing it to interact with Rust Result, we can consequentially have better error handling solution.

  1. User may use any self-defined type or imported type as exception type, and can create exceptions with throw, can catch and handle exceptions with try - catch.

    // Pseudo code
    // there's no way to define custom types within script currently.
    // type Exception = ...;
    
    func new_exception() Exception {
        // implementation goes here
    }
    
    func handle_exception(e Exception) {
        // implementation goes here
    }
    
    // try block
    try {
        throw new_exception(); // throwing an exception
    } catch (e Exception) {    // catching an exception
        handle_exception(e);   // handling an exc eption
    }
  2. Different exception types should be caught with different catch clauses with corresponding types. Catch clause with any type could catch exceptions of arbitrary type.

    // type E1 = ...;
    // type E2 = ...;
    
    // ...
    
    try {
        may_throw_e1();
        may_throw_e2();
    } catch (e1 E1) {
        handle_e1(e1);
    } catch (e2 E2) {
        handle_e2(e2);
    }
  3. Within a function, an exception must be either explicitly caught, or explicitly marked with exception specification. Marking an exception that will never be thrown is disallowed.

    // Okay, all exceptions caught
    func f1() {
        try {
            may_throw_e1();
            may_throw_e2();
        } catch(e1 E1) {
            handle_e1(e1);
        } catch(e2 E2) {
            handle_e2(e2);
        }
    }
    
    // Okay, all exceptions marked
    func f2() throws(E1, E2) {
        may_throw_e1();
        may_throw_e2();
    }
    
    // Okay
    func f3() throws(E1) {
        try {
            may_throw_e1();
            may_throw_e2();
        } catch (e2 E2) {
            handle_e2(e2);
        }
    }
    
    // Ill-formed, E1 not handled or marked
    func f4() {
        try {
            may_throw_e1();
            may_throw_e2();
        } catch (e2 E2) {
            handle_e2(e2);
        }
    }
    
    // Ill-formed, E1 will never be thrown
    func f5() throws(E1) {
        // do nothing
    }
    
    // Ill-formed, E1 already caught, and thus will never be thrown
    func f6() throws(E1) {
        try {
            may_throw_e1();
        } catch (e1 E1) {
            handle_e1(e1);
        }
    }
  4. Two functions only differ on exception specifications does not constitute overloading.

    // Ill-formed, duplicated instead of overloaded
    func foo() throw(E1, E2) {
        // implementation goes here
    }
    
    func foo() {
        // implementation goes here
    }
  5. For Rust functions that return Result<T, E> types, when fetching the return value of taht function, if the Result contains an Err(E), the inner E will be transformed into an Pr47 exception.

Checked exceptions have been criticized for almost twenty years [1] [2]. However, I together with Yinwang [3], don't think so. The descedant languages (C# and Kotlin) who attacked checked exceptions are even unable to provide a mechanism of equivalent good.

Even with checked exceptions, user may use throws(any) to turn checked into unchecked. However, if the language provide unchecked exception by itself, there's no way to use it in a checked way within the language.

What's more, it is possible to disable exception specification checking with attributes in Pr47, unlike Java. If a user finds the check useless, just disable it, and there won't be any problem.


Unresolved problems

Fill this field if found


See also

  1. Error handling of Rhai: https://schungx.github.io/rhai/language/throw.html
  2. Error handling of Lua: https://www.lua.org/pil/8.4.html
chuigda commented 3 years ago

[JUG] Does anyone have better solution than exceptions?

chuigda commented 3 years ago

[Phosphorus] One of the weird point of such checking is: you have to declare an exception that logically cannot be thrown out.

if (false) {
  throw new SomeException();
}

The case above is simple enough so it can be easily resolved via static analysis. Not all cases can be statically analyzed, however.

chuigda commented 3 years ago

[SchrodingerZhu] I suggest using algebraic effects. Here's a tutorial. https://dev.to/yelouafi/algebraic-effects-in-javascript-part-1---continuations-and-control-transfer-3g88

chuigda commented 3 years ago

[MID] It is also possible to introduce Result type to Pr47. I'll create another proposal then.

chuigda commented 3 years ago

[SchrodingerZhu] I suggest using algebraic effects. Here's a tutorial.

[MID] Functional programming is crap.

chuigda commented 3 years ago

[Phosphorus] One of the weird point of such checking is: you have to declare an exception that logically cannot be thrown out.

if (false) {
  throw new SomeException();
}

The case above is simple enough so it can be easily resolved via static analysis. Not all cases can be statically analyzed, however.

[msk] This can be resolved with a catch clause with an unreachable.

chuigda commented 3 years ago

[MID] Since it seems that there's no critical problem with this solution, and currently we don't have better solution than this, I think we can start standardizing exception handling.

chuigda commented 3 years ago

[JUG] Here comes a design issue: should unchecked exceptions be recoverable? If we'd like to simplify the problem, we can make unchecked exceptions non-recoverable, and we can write out simple and efficient implementation. As far as we know panic! in Rust is also almost not recoverable.

chuigda commented 3 years ago

[JUG] Here comes a design issue: should unchecked exceptions be recoverable?

[SUP] This is configurable, either at compile time or run time.

chuigda commented 3 years ago

[T10] We've found that adding checked exceptions may add some overhead to runtime implementation, even if user does not use exception mechanism at all. We'll continue to focus on this.

chuigda commented 3 years ago

[JUG] Here comes a design issue: should unchecked exceptions be recoverable?

[SUP] This is configurable, either at compile time or run time.

[T10] Making unchecked exception recoverable can significantly increase the complexity of static analysis.

chuigda commented 3 years ago

[MID] The implementation team is on the treasure hunt: https://github.com/Pr47/Pr47/pull/15.

CousinZe commented 3 years ago

User-defined types cannot carry stack-trace or relevant information. I suggest adding an exc<T> container type. This will somewhat increase the complexity, but fine.

chuigda commented 3 years ago

[MID] Stack traces might be less useful. But fine, I'll take this into consideration.