Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Named binding patterns name^pattern in pattern matching, function parameters, declarations, for loops; fix complex bindings in for loops #1668

Merged
merged 18 commits into from
Jan 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 81 additions & 20 deletions civet.dev/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,28 @@ a + b = c
++count *= 2
</Playground>

### Multi Destructuring

Use `name^pattern` to assign `name` while also destructuring into `pattern`:

<Playground>
[first^{x, y}, ...rest] = points
</Playground>

Shorthand for destructuring an object property and its contents:

<Playground>
{name^: {first, last}} = person
</Playground>

::: info
Multi destructuring also works in
[declarations](#variable-declaration),
[function parameters](#parameter-multi-destructuring),
[`for` loops](#for-loop-multi-destructuring), and
[pattern matching](#pattern-matching).
:::

### Humanized Operators

<Playground>
Expand Down Expand Up @@ -1153,6 +1175,17 @@ This is particularly useful within methods.
@promise := new Promise (@resolve, @reject) =>
</Playground>

### Parameter Multi Destructuring

[Multi destructuring](#multi-destructuring) applies to function parameters:

<Playground>
function Component(props^{
name^: {first, last},
counter
})
</Playground>

### `return.value`

Instead of specifying a function's return value when it returns,
Expand Down Expand Up @@ -1598,9 +1631,7 @@ switch x
console.log "leading type:", type
</Playground>

::: info
You can also use condition fragments as patterns.
:::
You can also use condition fragments as patterns:

<Playground>
switch x
Expand All @@ -1614,31 +1645,27 @@ switch x
console.log "it's something else"
</Playground>

You can add a binding before a condition fragment:

<Playground>
switch x
% 15 is 0
console.log "fizzbuzz"
% 3 is 0
console.log "fizz"
% 5 is 0
console.log "buzz"
else
console.log x
switch f()
x % 15 is 0
console.log "fizzbuzz", x
x % 3 is 0
console.log "fizz", x
x % 5 is 0
console.log "buzz", x
</Playground>

::: info
Aliasing object properties works the same as destructuring.
:::
Aliasing object properties works the same as destructuring:

<Playground>
switch e
{type, key: eventKey}
return [type, eventKey]
</Playground>

::: info
Patterns can aggregate duplicate bindings.
:::
Patterns can aggregate duplicate bindings:

<Playground>
switch x
Expand All @@ -1657,6 +1684,16 @@ switch x
console.log type, content, first
</Playground>

More generally, use `name^pattern` or `name^ pattern`
([multi destructuring](#multi-destructuring))
to bind `name` while also matching `pattern`:

<Playground>
switch x
[space^ /^\s*$/, number^ /^\d+$/, ...]
console.log space, number
</Playground>

Use `^x` to refer to variable `x` in the parent scope,
as opposed to a generic name that gets destructured.
(This is called "pinning" in
Expand Down Expand Up @@ -2094,6 +2131,27 @@ rateLimits := {
}
</Playground>

### For Loop Multi Destructuring

[Multi destructuring](#multi-destructuring) applies to `for..of/in` loops:

<Playground>
for item^[key, value] of map
if value and key.startsWith "a"
process item
</Playground>

<Playground>
for person^{name^: {first, last}, age} of people
console.log first, last, age, person
</Playground>

<Playground>
for key, value^{x, y} in items
if x > y
process key, value
</Playground>

### Infinite Loop

<Playground>
Expand Down Expand Up @@ -2261,10 +2319,13 @@ You can also specify multiple `catch` blocks using
<Playground>
try
foo()
catch e <? MyError
console.log "MyError", e.data
catch <? RangeError, <? ReferenceError
console.log "R...Error"
catch {message: /bad/}
console.log "bad"
catch e^{message^: /bad/}
console.log "bad", message
throw e
catch e
console.log "other", e
</Playground>
Expand Down
78 changes: 59 additions & 19 deletions source/parser.hera
Original file line number Diff line number Diff line change
Expand Up @@ -1475,6 +1475,7 @@ LeftHandSideExpression
children: $0,
expression,
}
NamedBindingPattern
CallExpression
# NOTE: OptionalExpression is merged into CallExpression

Expand Down Expand Up @@ -1983,7 +1984,7 @@ FunctionRestParameter

# NOTE: Similar to BindingElement but appears in formal parameters list
ParameterElement
_? AccessModifier?:accessModifier _? ( NWBindingIdentifier / BindingPattern ):binding TypeSuffix?:typeSuffix Initializer?:initializer ParameterElementDelimiter:delim ->
_? AccessModifier?:accessModifier _? ( BindingPattern / NWBindingIdentifier ):binding TypeSuffix?:typeSuffix Initializer?:initializer ParameterElementDelimiter:delim ->
typeSuffix ??= binding.typeSuffix
return {
type: "Parameter",
Expand Down Expand Up @@ -2067,13 +2068,29 @@ PinPattern
expression,
}

# `name^ pattern` means bind the whole thing to `name`,
# but also destructure/match `pattern`
NamedBindingPattern
BindingIdentifier:binding Caret _?:ws BindingPattern:pattern ->
pattern = prepend(ws, pattern)
return {
type: "NamedBindingPattern",
// NOTE: children just has binding, not pattern, for easy destructuring
children: [binding],
binding,
pattern,
subbinding: [pattern, " = ", binding],
typeSuffix: pattern.typeSuffix,
}

# https://262.ecma-international.org/#prod-BindingPattern
BindingPattern
ObjectBindingPattern
ArrayBindingPattern
PinPattern
Literal
RegularExpressionLiteral
NamedBindingPattern

# https://262.ecma-international.org/#prod-ObjectBindingPattern
# NOTE: Simplified from spec
Expand Down Expand Up @@ -2175,7 +2192,21 @@ BindingProperty

# NOTE: Allow ::T type suffix before value
# NOTE: name^ means we should bind name despite having a value
_?:ws1 PropertyName:name Caret?:bind _?:ws2 Colon:colon _?:ws3 ( BindingIdentifier / BindingPattern ):value BindingTypeSuffix?:typeSuffix Initializer?:initializer ->
_?:ws1 PropertyName:name Caret?:bind _?:ws2 Colon:colon _?:ws3 ( BindingPattern / BindingIdentifier ):value BindingTypeSuffix?:typeSuffix Initializer?:initializer ->
// Convert `name^: pattern` into `name: name^pattern`
if (bind) {
const binding = name, pattern = value
value = {
type: "NamedBindingPattern",
// NOTE: children just has binding, not pattern, for easy destructuring
children: [binding],
binding,
pattern,
subbinding: [pattern, " = ", binding],
typeSuffix: pattern.typeSuffix,
names: value.names,
}
}
return {
type: "BindingProperty",
children: [ws1, name, ws2, colon, ws3, value, initializer], // omit typeSuffix
Expand All @@ -2184,7 +2215,6 @@ BindingProperty
typeSuffix,
initializer,
names: value.names,
bind: !!bind,
}

# NOTE: ^name is short for property `name: ^name`
Expand Down Expand Up @@ -2247,7 +2277,6 @@ BindingProperty
initializer,
names: binding.names,
identifier: binding,
bind: !!bind,
}

# https://262.ecma-international.org/#prod-BindingRestProperty
Expand Down Expand Up @@ -2292,7 +2321,7 @@ BindingElement
BindingRestElement

# NOTE: Merged in SingleNameBinding
_?:ws ( BindingIdentifier / BindingPattern ):binding BindingTypeSuffix?:typeSuffix Initializer?:initializer ->
_?:ws ( BindingPattern / BindingIdentifier ):binding BindingTypeSuffix?:typeSuffix Initializer?:initializer ->
return {
type: "BindingElement",
names: binding.names,
Expand All @@ -2312,7 +2341,7 @@ BindingElement

# https://262.ecma-international.org/#prod-BindingRestElement
BindingRestElement
_?:ws DotDotDot:dots ( BindingIdentifier / BindingPattern / EmptyBindingPattern ):binding BindingTypeSuffix?:typeSuffix ->
_?:ws DotDotDot:dots ( BindingPattern / BindingIdentifier / EmptyBindingPattern ):binding BindingTypeSuffix?:typeSuffix ->
return {
type: "BindingRestElement",
children: [ws, dots, binding],
Expand All @@ -2324,7 +2353,7 @@ BindingRestElement
rest: true,
}

_?:ws ( BindingIdentifier / BindingPattern ):binding DotDotDot:dots ->
_?:ws ( BindingPattern / BindingIdentifier ):binding DotDotDot:dots ->
return {
type: "BindingRestElement",
children: [...(ws || []), dots, binding],
Expand Down Expand Up @@ -4993,10 +5022,16 @@ ForDeclaration
# But don't add for member expressions like a.x,
# which parse as a pinned pattern; leave those for LeftHandSideExpression
InsertConst:c !ActualMemberExpression ForBinding:binding ->
// Avoid parsing something like [x.y] as an array pattern with a pin pattern
// (leaving them to parse as LeftHandSideExpression)
// Also actual pins like ^x don't make sense in for declaration
if (gatherRecursive(binding, $ => $.type === "PinPattern").length) {
return $skip
}
return {
type: "ForDeclaration",
children: [c, binding],
decl: c.token,
decl: c.token.trimEnd(),
binding,
names: binding.names,
}
Expand Down Expand Up @@ -5132,12 +5167,17 @@ PatternExpressionList

PatternExpression
BindingPattern
ForbidIndentedApplication ( SingleLineBinaryOpRHS+ )?:pattern RestoreIndentedApplication ->
if (!pattern) return $skip
ForbidIndentedApplication ( ( BindingIdentifier Caret? )? SingleLineBinaryOpRHS+ )?:body RestoreIndentedApplication ->
if (!body) return $skip
const [ named, rhs ] = body
let binding
if (named) [ binding ] = named

return {
type: "ConditionFragment",
children: pattern,
children: [binding, rhs],
binding,
rhs,
}

CaseExpressionList
Expand Down Expand Up @@ -5220,13 +5260,6 @@ FinallyClause

# https://262.ecma-international.org/#prod-CatchParameter
CatchParameter
BindingIdentifier:binding TypeSuffix?:typeSuffix ->
return {
type: "CatchParameter",
binding,
typeSuffix,
children: $0,
}
( ObjectBindingPattern / ArrayBindingPattern ):binding TypeSuffix:typeSuffix ->
return {
type: "CatchParameter",
Expand All @@ -5240,6 +5273,13 @@ CatchParameter
children: $0,
patterns: $1,
}
BindingIdentifier:binding TypeSuffix?:typeSuffix ->
return {
type: "CatchParameter",
binding,
typeSuffix,
children: $0,
}

# An expression with explicit or implied parentheses, for use in if/while/switch
Condition
Expand Down Expand Up @@ -6547,7 +6587,7 @@ By
return { $loc, token: $1 }

Caret
"^" ->
"^" !"^" ->
return { $loc, token: $1 }

Case
Expand Down
26 changes: 26 additions & 0 deletions source/parser/binding.civet
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import type {
ASTNode
ASTNodeObject
BindingPattern
BindingProperty
BindingRestElement
Children
HasSubbinding
ObjectBindingPattern
ThisAssignments
} from ./types.civet
Expand Down Expand Up @@ -107,6 +109,28 @@ function adjustBindingElements(elements: ASTNodeObject[])
length
}

/**
Find and return all `subbinding` properties, prefixed with commas,
including searching within those subbindings for more subbindings
*/
function gatherSubbindings(node: ASTNode, subbindings: ASTNode[] = []): ASTNode[]
for each p of gatherRecursiveAll node, ($): $ is HasSubbinding => ($ as HasSubbinding).subbinding?
{ subbinding } := p
subbindings.push ", ", subbinding
gatherSubbindings subbinding, subbindings
subbindings

/**
Simplify `{name: name}` to just `{name}` that results from
the parser expanding `{name^: pattern}` into `{name: name^pattern}`
*/
function simplifyBindingProperties(node: ASTNode)
for each p of gatherRecursiveAll node, .type is "BindingProperty"
{ name, value } := p
if value?.type is "NamedBindingPattern" and value.binding is name
[ws] := p.children
p.children = [ws, name, p.delim]

function gatherBindingCode(statements: ASTNode, opts?: { injectParamProps?: boolean, assignPins?: boolean })
thisAssignments: ThisAssignments := []
splices: unknown[] := []
Expand Down Expand Up @@ -234,6 +258,8 @@ function gatherBindingPatternTypeSuffix(pattern: ArrayBindingPattern | ObjectBin
export {
adjustAtBindings
adjustBindingElements
gatherSubbindings
gatherBindingCode
gatherBindingPatternTypeSuffix
simplifyBindingProperties
}
Loading
Loading