Skip to content

Commit

Permalink
Implement 'errdefer'
Browse files Browse the repository at this point in the history
I implemented the 'defer' syntax earlier, and it's been pretty handy,
but I've quite often hit the use case of only running defer statements
if the script *fails*. That's precisely what defer does. I document it a
bit on the MkDocs page. Here's a demo though:

  defer:
    print(1)
    print(2)
  errdefer:
    print(3)
    print(4)
  defer:
    print(5)
    print(6)
  errdefer:
    print(7)
    print(8)
  print("Hello!")
  exit(0)  // successful script run

outputs

  Hello!
  5
  6
  1
  2

i.e. errdefers don't run. The failure case:

  defer:
    print(1)
    print(2)
  errdefer:
    print(3)
    print(4)
  defer:
    print(5)
    print(6)
  errdefer:
    print(7)
    print(8)
  print("Hello!")
  exit(1)  // perceived as error!

outputs

  Hello!
  7
  8
  5
  6
  3
  4
  1
  2
  • Loading branch information
amterp committed Nov 12, 2024
1 parent 8c62596 commit 6fe0e8a
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 14 deletions.
2 changes: 1 addition & 1 deletion core/cobra_root.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ func registerInterpreterWithExit(interpreter *MainInterpreter) {
}
exiting = true
codeToExitWith = code
interpreter.ExecuteDeferredStmts()
interpreter.ExecuteDeferredStmts(code)
existing(codeToExitWith)
}
}
Expand Down
2 changes: 2 additions & 0 deletions core/gen_stmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ func (e DeleteStmt) String() string {

type DeferStmt struct {
DeferToken Token
IsErrDefer bool
DeferredStmt *Stmt
DeferredBlock *Block
}
Expand All @@ -337,6 +338,7 @@ func (e DeferStmt) Accept(visitor StmtVisitor) {
func (e DeferStmt) String() string {
var parts []string
parts = append(parts, fmt.Sprintf("DeferToken: %v", e.DeferToken))
parts = append(parts, fmt.Sprintf("IsErrDefer: %v", e.IsErrDefer))
parts = append(parts, fmt.Sprintf("DeferredStmt: %v", e.DeferredStmt))
parts = append(parts, fmt.Sprintf("DeferredBlock: %v", e.DeferredBlock))
return fmt.Sprintf("DeferStmt(%s)", strings.Join(parts, ", "))
Expand Down
2 changes: 1 addition & 1 deletion core/generators/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func main() {
"BreakStmt : Token BreakToken",
"ContinueStmt : Token ContinueToken",
"DeleteStmt : Token DeleteToken, []VarPath Vars",
"DeferStmt : Token DeferToken, *Stmt DeferredStmt, *Block DeferredBlock",
"DeferStmt : Token DeferToken, bool IsErrDefer, *Stmt DeferredStmt, *Block DeferredBlock",
})

defineAst(outputDir, "ArgStmt", "", []string{
Expand Down
6 changes: 5 additions & 1 deletion core/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,11 +498,15 @@ func (i *MainInterpreter) resolveStartEnd(sliceAccess SliceAccess, len int) (int
}

// todo currently these execute after an error is printed. Should they execute before?
func (i *MainInterpreter) ExecuteDeferredStmts() {
func (i *MainInterpreter) ExecuteDeferredStmts(errCode int) {
// execute backwards (LIFO)
for j := len(i.deferredStmts) - 1; j >= 0; j-- {
deferredStmt := i.deferredStmts[j]

if deferredStmt.IsErrDefer && errCode == 0 {
continue
}

func() {
defer func() {
if r := recover(); r != nil {
Expand Down
15 changes: 8 additions & 7 deletions core/keywords.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ var GLOBAL_KEYWORDS = map[string]TokenType{
"if": IF,
"else": ELSE,
//"resource": RESOURCE,
"del": DELETE,
"not": NOT,
"unsafe": UNSAFE,
"quiet": QUIET,
"fail": FAIL,
"recover": RECOVER,
"defer": DEFER,
"del": DELETE,
"not": NOT,
"unsafe": UNSAFE,
"quiet": QUIET,
"fail": FAIL,
"recover": RECOVER,
"defer": DEFER,
"errdefer": ERRDEFER,
}

var ARGS_BLOCK_KEYWORDS = map[string]TokenType{
Expand Down
16 changes: 12 additions & 4 deletions core/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ func (p *Parser) statement() Stmt {
return p.deleteStmt()
}

if p.peekKeyword(GLOBAL_KEYWORDS, DEFER) {
if p.peekKeyword(GLOBAL_KEYWORDS, DEFER) || p.peekKeyword(GLOBAL_KEYWORDS, ERRDEFER) {
return p.deferStmt()
}

Expand Down Expand Up @@ -526,13 +526,21 @@ func (p *Parser) deleteStmt() Stmt {
}

func (p *Parser) deferStmt() Stmt {
deferToken := p.consumeKeyword(GLOBAL_KEYWORDS, DEFER)
var deferToken Token
isErrDefer := false
if p.matchKeyword(GLOBAL_KEYWORDS, ERRDEFER) {
deferToken = p.previous()
isErrDefer = true
} else {
deferToken = p.consumeKeyword(GLOBAL_KEYWORDS, DEFER)
}

if p.matchAny(COLON) {
block := p.block()
return &DeferStmt{DeferToken: deferToken, DeferredStmt: nil, DeferredBlock: &block}
return &DeferStmt{DeferToken: deferToken, IsErrDefer: isErrDefer, DeferredStmt: nil, DeferredBlock: &block}
} else {
stmt := p.statement()
return &DeferStmt{DeferToken: deferToken, DeferredStmt: &stmt, DeferredBlock: nil}
return &DeferStmt{DeferToken: deferToken, IsErrDefer: isErrDefer, DeferredStmt: &stmt, DeferredBlock: nil}
}
}

Expand Down
1 change: 1 addition & 0 deletions core/token_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ const (
FAIL TokenType = "FAIL"
RECOVER TokenType = "RECOVER"
DEFER TokenType = "DEFER"
ERRDEFER TokenType = "ERRDEFER"
MAP TokenType = "MAP"

// only in Args block
Expand Down
84 changes: 84 additions & 0 deletions docs-web/docs/defer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
title: Defer & Errdefer
---

- `defer` and `errdefer` run in LIFO order, each kind being part of the same one queue.
- If there are several defer statements, and one fails, further defer statements will still attempt to run.
- Rad's error code will become an error if the main script succeeded but a defer statement failed.
- errdefers will not get triggered if the main script succeeded but a `defer` or `errdefer` statement failed.

## `defer`

```rsl title="defer Example"
defer:
print(1)
print(2)
defer:
print(3)
print(4)
print("Hello!")
```

```title="defer Example Output"
Hello!
3
4
1
2
```

## `errdefer`

```rsl title="errdefer Example 1"
defer:
print(1)
print(2)
errdefer:
print(3)
print(4)
defer:
print(5)
print(6)
errdefer:
print(7)
print(8)
print("Hello!")
exit(0) // successful script run
```

```title="errdefer Example 1 Output"
Hello!
5
6
1
2
```

```rsl title="errdefer Example 2"
defer:
print(1)
print(2)
errdefer:
print(3)
print(4)
defer:
print(5)
print(6)
errdefer:
print(7)
print(8)
print("Hello!")
exit(1) // perceived as error!
```

```title="errdefer Example 2 Output"
Hello!
7
8
5
6
3
4
1
2
```

0 comments on commit 6fe0e8a

Please sign in to comment.