Skip to content

Commit

Permalink
Merge pull request #1477 from DanielXMoore/catch-pattern
Browse files Browse the repository at this point in the history
Pattern matching `catch`
  • Loading branch information
edemaine authored Oct 21, 2024
2 parents 0c50478 + c8442b4 commit 7f00ea6
Show file tree
Hide file tree
Showing 10 changed files with 290 additions and 25 deletions.
2 changes: 1 addition & 1 deletion build/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ civet --no-config build/esbuild.civet "$@"

# built types
for name in astro esbuild rollup unplugin vite webpack; do
sed 's/\.civet"/\.js"/' dist/unplugin/source/unplugin/$name.civet.d.ts >dist/unplugin/$name.d.ts
sed 's/\.civet"/\.js"/' dist/unplugin/source/unplugin/$name.d.ts >dist/unplugin/$name.d.ts
done
rm -rf dist/unplugin/source

Expand Down
23 changes: 23 additions & 0 deletions civet.dev/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1916,6 +1916,29 @@ else
callback result
</Playground>
You can also specify multiple `catch` blocks using
[pattern matching](#pattern-matching):
<Playground>
try
foo()
catch <? RangeError, <? ReferenceError
console.log "R...Error"
catch {message: /bad/}
console.log "bad"
catch e
console.log "other", e
</Playground>
If you omit a catch-all at the end,
the default behavior is to re-`throw` the error:
<Playground>
try
foo()
catch {message: /^EPIPE:/}
</Playground>
### Do Blocks
To put multiple lines in a scope and possibly an expression,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
"unplugin": "^1.12.2"
},
"devDependencies": {
"@danielx/civet": "0.7.30",
"@danielx/civet": "0.8.4",
"@danielx/hera": "^0.8.16",
"@prettier/sync": "^0.5.2",
"@types/assert": "^1.5.6",
Expand Down
53 changes: 41 additions & 12 deletions source/parser.hera
Original file line number Diff line number Diff line change
Expand Up @@ -4814,12 +4814,12 @@ CaseClause
children: $0
}
# NOTE: Added else from CoffeeScript
Else ImpliedColon ( ThenClause / BracedBlock / EmptyBlock ):block ->
$1.token = "default"
Else:e ImpliedColon:colon ( ThenClause / BracedBlock / EmptyBlock ):block ->
e = { ...e, token: "default" }
return {
type: "DefaultClause",
block,
children: $0
children: [ e, colon, block ]
}

PatternExpressionList
Expand Down Expand Up @@ -4871,24 +4871,35 @@ IgnoreColon

# https://262.ecma-international.org/#prod-TryStatement
TryStatement
Try !":" NoPostfixBracedOrEmptyBlock CatchClause? ElseClause? FinallyClause? ->
Try !":" NoPostfixBracedOrEmptyBlock CatchClause* ElseClause? FinallyClause? ->
return processTryBlock($0)

# https://262.ecma-international.org/#prod-Catch
CatchClause
( Nested / _ ) Catch CatchBind? ( BracedThenClause / BracedOrEmptyBlock ):block ->
( Nested / _ ) Catch CatchBinding?:binding ( BracedThenClause / BracedOrEmptyBlock ):block ->
return {
type: "CatchClause",
children: $0,
block,
binding,
}

# NOTE: Added optional parentheses to catch binding
CatchBind
_? OpenParen __ CatchParameter __ CloseParen
_:ws InsertOpenParen:open !EOS ForbidIndentedApplication CatchParameter?:param RestoreIndentedApplication InsertCloseParen:close ->
if (!param) return $skip
return [ ws, open, param, close ]
CatchBinding
_?:ws1 OpenParen:open __:ws2 AllowAll CatchParameter?:parameter RestoreAll __:ws3 CloseParen:close ->
if (!parameter) return $skip
return {
type: "CatchBinding",
parameter,
children: [ ws1, open, ws2, parameter, ws3, close ],
}
_:ws InsertOpenParen:open !EOS ForbidIndentedApplication CatchParameter?:parameter RestoreIndentedApplication InsertCloseParen:close ->
if (!parameter) return $skip
return {
type: "CatchBinding",
parameter,
children: [ ws, open, parameter, close ]
}

# https://262.ecma-international.org/#prod-Finally
FinallyClause
Expand All @@ -4901,8 +4912,26 @@ FinallyClause

# https://262.ecma-international.org/#prod-CatchParameter
CatchParameter
BindingIdentifier TypeSuffix?
BindingPattern TypeSuffix?
BindingIdentifier:binding TypeSuffix?:typeSuffix ->
return {
type: "CatchParameter",
binding,
typeSuffix,
children: $0,
}
( ObjectBindingPattern / ArrayBindingPattern ):binding TypeSuffix:typeSuffix ->
return {
type: "CatchParameter",
binding,
typeSuffix,
children: [ binding, typeSuffix ],
}
PatternExpressionList ->
return {
type: "CatchPattern",
children: $0,
patterns: $1,
}

# An expression with explicit or implied parentheses, for use in if/while/switch
Condition
Expand Down
116 changes: 113 additions & 3 deletions source/parser/lib.civet
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ import type {
ASTNodeParent
ASTRef
AssignmentExpression
Bindings
BlockStatement
CallExpression
CaseBlock
CatchBinding
CatchClause
ComptimeStatement
Condition
Expand All @@ -25,14 +28,17 @@ import type {
ExpressionNode
FinallyClause
ForStatement
FunctionExpression
IfStatement
Initializer
IterationStatement
MemberExpression
MethodDefinition
NormalCatchParameter
ParenthesizedExpression
Placeholder
StatementExpression
StatementTuple
SwitchStatement
TypeArguments
TypeSuffix
WSNode
Expand Down Expand Up @@ -335,8 +341,112 @@ function handleThisPrivateShorthands(value)

return [value, value.thisShorthand]

function processTryBlock($0: [ASTLeaf, undefined, BlockStatement, CatchClause?, ElseClause?, FinallyClause?])
[t, , b, c, e, f] .= $0
function processTryBlock($0: [ASTLeaf, undefined, BlockStatement, CatchClause[], ElseClause?, FinallyClause?])
[t, , b, cs, e, f] .= $0

// Pattern matching catch clauses: transform into pattern matching switch
let c: CatchClause?
if cs.some .binding?.parameter is like {type: "CatchPattern"}
ref := makeRef("e")
binding: CatchBinding :=
type: "CatchBinding"
children: [ "(", ref, ")" ]
parameter: ref
condition: ParenthesizedExpression :=
type: "ParenthesizedExpression"
children: [ "(", ref, ")" ]
expression: ref
defaultClause .= false
clauses: CaseBlock["clauses"] := cs.map (clause) =>
if {type: "CatchPattern", patterns} := clause.binding?.parameter
{
type: "PatternClause"
patterns
block: clause.block
children: [ patterns, clause.block ]
}
else
defaultClause = true
parameter := clause.binding?.parameter
if parameter?
assert.equal parameter.type, "CatchParameter",
`Invalid catch parameter ${parameter.type}`
{ binding: pattern, typeSuffix } := parameter as NormalCatchParameter
initializer: Initializer :=
type: "Initializer"
expression: ref
children: [ "", " = ", ref ]
bindings: Bindings[] := [{
type: "Binding"
names: pattern.names
pattern
typeSuffix
initializer
children: [ pattern, typeSuffix, initializer ]
splices: []
thisAssignments: []
}]
clause.block.expressions.unshift ["", {
type: "Declaration"
children: ["let", " ", bindings]
bindings
names: bindings[0].names
decl: "let"
}, ";"]

type: "DefaultClause"
block: clause.block
children: [ "default: ", clause.block ]

// If no default clause specified, add one that rethrows exception
unless defaultClause
expressions: StatementTuple[] := [["",
type: "ThrowStatement"
children: [ "throw", " ", ref ]
]]
block: BlockStatement := {
type: "BlockStatement"
expressions
children: [ " {", expressions, "}" ]
bare: false
}
clauses.push {
type: "DefaultClause"
block
children: [ "default: ", block ]
}
caseBlock: CaseBlock := {
type: "CaseBlock"
clauses
children: [ " {", clauses, "}" ]
}
patternSwitch: SwitchStatement := {
type: "SwitchStatement"
condition
caseBlock
children: [ "switch", condition, caseBlock ]
}
block: BlockStatement :=
type: "BlockStatement"
bare: false
expressions: [["", patternSwitch]]
children: [" {", patternSwitch, "}"]
c = makeNode {
type: "CatchClause"
children:
. cs[0].children[0] // whitespace
. cs[0].children[1] // catch token
. binding, block
binding
block
}
else
c = cs[0]
if cs# > 1
c = append c,
type: "Error"
message: "Only one catch clause allowed unless using pattern matching"
as CatchClause

// Default behavior catches all exceptions
if !c and (e or !f)
Expand Down
35 changes: 33 additions & 2 deletions source/parser/types.civet
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,14 @@ export type OtherNode =
| Binding
| BindingRestElement
| Call
| CatchBinding
| CatchClause
| CatchPattern
| CaseBlock
| CaseClause
| CommentNode
| ComputedPropertyName
| ConditionFragment
| DefaultClause
| ElseClause
| FieldDefinition
Expand All @@ -91,9 +95,11 @@ export type OtherNode =
| Initializer
| Label
| NonNullAssertion
| NormalCatchParameter
| ObjectBindingPattern
| Parameter
| ParametersNode
| PatternClause
| PinPattern
| PinProperty
| Placeholder
Expand Down Expand Up @@ -590,11 +596,36 @@ export type TryStatement

export type CatchClause
type: "CatchClause"
children: Children & [ Whitespace | ASTString, CatchToken, BlockStatement ]
children: Children & [ Whitespace | ASTString, CatchToken, CatchBinding, BlockStatement ]
parent?: Parent
block: BlockStatement
binding: CatchBinding?

export type CatchToken = "catch(e) " | { $loc: Loc, token: "finally" }
export type CatchToken = { $loc: Loc, token: "catch" }

export type CatchBinding
type: "CatchBinding"
children: Children
parent?: Parent
parameter: CatchParameter

export type CatchParameter =
| NormalCatchParameter
| CatchPattern
| ASTRef // made by processTryBlock for pattern matching catch

export type NormalCatchParameter =
type: "CatchParameter"
children: Children
parent?: Parent
binding: ObjectBindingPattern | ArrayBindingPattern
typeSuffix: TypeSuffix?

export type CatchPattern
type: "CatchPattern"
children: Children
parent?: Parent
patterns: PatternExpression[]

export type FinallyClause
type: "FinallyClause"
Expand Down
2 changes: 1 addition & 1 deletion source/parser/util.civet
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {

// Types need to be upfront to allow for TypeScript `asserts`
assert: {
equal(a: unknown, b: unknown, msg: string): void
equal(a: unknown, b: unknown, msg: string): asserts a is b
notEqual(a: unknown, b: unknown, msg: string): void
notNull<T>(a: T, msg: string): asserts a is NonNullable<T>
} := {
Expand Down
Loading

0 comments on commit 7f00ea6

Please sign in to comment.