twinbasic / lang-design

Language Design for twinBASIC
MIT License
11 stars 1 forks source link

We need Try/Finally keywords #61

Open bclothier opened 2 years ago

bclothier commented 2 years ago

Is your feature request related to a problem? Please describe. twinBASIC provides Return which allow us to leave the function immediately. However, there is no way to guarantee proper clean up. In other languages, this is usually managed with a Try/Finally block. I know that it is planned to integrate vbWatchDog which does provide both ErrEx.Catch and ErrEx.Finally but no Try keyword.

Considering that there is also an open request for block scoping ( twinbasic/lang-design#33 ), that may be one way to help make it easier to write code that won't be so deeply arrowed due to mixing of error handling & control flow & cleaning.

Describe the solution you'd like At a minimum, implement the equivalent of Finally or ErrEx.Finally to make it easy to define a block of code that must be run no matter what.

Describe alternatives you've considered We still can achieve similar thing using traditional error handling, but instead of doing Exit Sub, we'd need to use GoTo CleanUp or GoTo ExitProc to guarantee the cleanup on an early exit, and be careful to use Resume CleanUp when in an error handler block which isn't all that great for language consistency.

WaynePhillipsEA commented 2 years ago

Yes, this is an interesting area. In vbWatchdog for VBA/VB6 there was of course no option to add real block-scoping to the catch/finally blocks, but we do potentially have the option to change that in the tB version.

When we do get to this, one of my main concerns is the introduction of new keywords and the clashes that would entail. If we introduce real Try, Catch, and Finally keywords, these are likely to clash with some existing codebases, but if we stick to the current vbWatchdog options of ErrEx.Catch, ErrEx.Finally (and now ErrEx.Try), then they are much less likely to. Or we could stick them on Err instead... Err.Try, Err.Catch, Err.Finally:

Public Sub DoSomething()
    Err.Try
        Debug.Print 1/0

    Err.Catch 11
         MsgBox "division by zero..."

    Err.CatchAll
        MsgBox "unexpected error..."

    Err.Finally
        MsgBox "cleaning up..."
End Sub

Just ideas. There's plenty of time to discuss on this.

Greedquest commented 2 years ago

Err.Try is of course weird because it doesn't look like a keyword, but Debug.Print sets a bit of a precedent in this regard, it's a slightly special method.

I prefer Err.NoError or Err.NoException (if we get structured exceptions) to Err.Finally - finally has always felt a bit weird to me because it sounds like it should run after everything, exception or not. Python has try... except...else, kinda like Case Else but that's a bit too vague imo and case else matches everything not captured by a previous block in VBA, whereas the NoError label is more like Catch 0 and uncaught errors get bubbled up. sorry got confused between finally and else, thanks @bclothier!

Greedquest commented 2 years ago

You could make it a two word keyword like With Try With Catch. Or make it a contextual keyword:

Sub foo()
  Try
    result = 1/0

  Catch 11
    MsgBox "division by zero..."

  CatchAll
    MsgBox "unexpected error..."

  NoError
    Debug.Print "Answer was: "; result

  Finally
    MsgBox "cleaning up..."

  End Try
  '[...]
End Sub

I.e. Try is only considered a reserved keyword (rather than a parameterless sub call) when matched by an End Try. Within that block Catch is also a keyword - it won't break existing code because it's only a keyword in the context of the try block.

This is how the new python match block works - they are only keywords in an unambiguous context, otherwise they can be used as normal.

bclothier commented 2 years ago

finally has always felt a bit weird to me because it sounds like it should run after everything, exception or not.

That's exactly the intention and is important especially in an unmanaged language like twinBASIC where we need to guarantee cleanup to avoid worse problems before leaving a routine, regardless whether it succeeded or had an error. An "NoErr" that only runs when there was no error wouldn't be very useful.

In traditional VBx, I consider it actually errorprone to set up a proper cleanup. To illustrate:

Public Sub DoThings()
  On Error GoTo ErrHandler

  If Precondition = False Then
    Exit Sub 'OKish
  End If

  Dim HotHandle As LongPtr 
  HotHandle = OpenHandle(...) 'watch out!

  If Some Condition = False Then
   Exit Sub 'Bad! The handle is still hot!
  End If

  If OtherCondition = False Then
    GoTo ExitProc 'Better, we close the handle
  End If

 'Some more code that could potentially raise an error...

ExitProc:
  'We must release the handle before leaving
  If HotHandle Then CloseHandle(HotHandle) 
  Exit Sub

ErrHandler:
  If Err.Number = X Then
    GoTo ExitProc 'Bad! Error still active
  ElseIf Err.Number = Y Then 
    Debug.Print "oh, well!" 
   'Bad, no resume and we fall out without cleaning up
  Else
    Resume ExitProc 'OK
  End If
End Sub

This code shows that we have to juggle between Exit Sub, GoTo <label>, Resume <label>, depending on where we are in the code, and using them in wrong places will cause problems at runtime. I don't think that's a very desirable way of ensuring that the HotHandle will have a CloseHandle called on it when leaving the method. More importantly, the CloseHandle is not a part of the error handling which further confuses the intention of what we need to do in this method. We just need it to happen no matter whether there was an error or not to avoid leaking a handle.

As one possible alternative to help separate the cleanup from the error handling is to allow something like this:

Dim HotHandle As LongPtr On Release CloseHandle(HotHandle)

But honestly I'm a bit iffy on this because the cleanup might require more complex logic (in the example above, we used a single-line If statement to test whether HotHandle is nonzero before calling CloseHandle on it.

One way to enhance the language without adding new keywords would be something like:

On Return Resume ExitProc

or

On Error Or Resume Resume ExitProc

This way, whether you use Exit Sub, Exit Function, Resume <label>, or Return <some value>, it will run the label regardless. The downside is that it's using a label rather than a block and things could get weird with multiple labels or multiple On...Resume statements. Also, one still can fall through into that block unintentionally.

For the record, I'm fine with either Err.Try or ErrEx.Try to avoid adding keywords if the proposed Try block is not an option.

bclothier commented 2 years ago

Just as a FYI, 'On' is another possible choice:

On Try
On Catch
On Finally
Greedquest commented 2 years ago

@bclothier Ah, my mistake, in python they have the else and finally and I think they're both useful actually (updated the other comment):

def divide(x, y):
    try:  # get the resource/value
        result = x / y
    except ZeroDivisionError:  # can't get the resource
        print("division by zero!")
    else:  # use the resource and clean it up
        print("result is", result)
    finally:  # cleanup that runs in all situations
        print("executing finally clause")

I don't think I've really used finally, just else

def divide(x, y):
    try:
        result = x / y
    except ZeroDivisionError:
        print("division by zero!")
    else:
        print("result is", result)

    print("executing finally clause")

else/NoError is perfect when opening a resource might make an error, and so you only need to close the resource if the try block suceeded:

Dim token As Long = RemovePassword("C:/file.csv") 'remove password to access file
Try
   Dim file As CSVFile = New CSVFile("C:/file.csv")

Catch ERR_OPEN_FILE 'this block only runs if there was an error opening the file
    Debug.Print "Could not open file, maybe it is readonly"

NoError 'this block only runs if we were successful opening the file
    Dim fileContents As String = file.readAll
    file.close

Finally
    RestorePassword token, "C:/file.csv"  'always restore password, regardless of whether file was openable

End Try
Debug.Print "Finished!"

Apparently finally is useful when you do an early Return (see here) as you mention On Return Resume CloseHandle

FullValueRider commented 2 years ago

Is there any possibility of elevating twinBasic above the try catch paradigm. WOuld it be possible to allow the definition of a local procedure which is called when the surrounding scope ends.

E.g. from @bclothiers example would it be possible to have something like a Termination procedure defined for specific variables within the 'DoThings' method.

Def Terminate HotHandle()

    'Do the stuff necessary to correctly close HotHandle

end Terminate.
DaveInCaz commented 2 years ago

@bclothier Re: comment https://github.com/twinbasic/lang-design/issues/61

In traditional VBx, I consider it actually errorprone to set up a proper cleanup. To illustrate:

FWIW we actually use gosub/return to sort of mimic a "finally" block. The pattern is like this:

Function DoSomething() As Boolean

    On Error Goto ErrHandler

    ...

    ' All exit points either Goto ExitFunction explicitly or just reach that point naturally

    ...

ExitFunction:
    GoSub Cleanup
    Exit Function

Cleanup:
    ' Close files, connections, whatever is needed
    Return

ErrHandler:
    TemporarilyStoreError ' Prevents Err details from being lost due to any cleanup operations
    GoSub Cleanup
    LogError ' records stored Err details in a pseudo-stack trace then raises it again, so the calling function's error handler gets triggered... etc. A bit like exception stack unwinding.
End Function

(With MZTools it was not hard to add this to code semi-automatically. Otherwise its hard to maintain)