Skip to content

Commit

Permalink
Merge pull request #1668 from DanielXMoore/caret
Browse files Browse the repository at this point in the history
Named binding patterns `name^pattern` in pattern matching, function parameters, declarations, `for` loops; fix complex bindings in `for` loops
  • Loading branch information
edemaine authored Jan 4, 2025
2 parents 3f69e98 + 53e0bd6 commit 33bcc40
Show file tree
Hide file tree
Showing 16 changed files with 571 additions and 116 deletions.
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

0 comments on commit 33bcc40

Please sign in to comment.