Skip to content

Commit

Permalink
Merge pull request #1663 from DanielXMoore/pattern-bind
Browse files Browse the repository at this point in the history
Pattern `name^: value` binds `name`, while `name: value` never does
  • Loading branch information
edemaine authored Dec 27, 2024
2 parents 072c016 + 2022014 commit 246bab8
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 83 deletions.
10 changes: 10 additions & 0 deletions civet.dev/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1646,6 +1646,16 @@ switch x
type
</Playground>
Object properties with value matchers are not bound by default (similar to
[object destructuring assignment](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment)).
Add a trailing `^` to bind them:
<Playground>
switch x
{type^: /list/, content^: [first, ...]}
console.log type, content, first
</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
76 changes: 43 additions & 33 deletions source/parser.hera
Original file line number Diff line number Diff line change
Expand Up @@ -2174,21 +2174,58 @@ BindingProperty
BindingRestProperty

# NOTE: Allow ::T type suffix before value
_? PropertyName:name _? Colon _? ( BindingIdentifier / BindingPattern ):value BindingTypeSuffix?:typeSuffix Initializer?:initializer ->
# 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 ->
return {
type: "BindingProperty",
children: [$1, name, $3, $4, $5, value, initializer], // omit typeSuffix
children: [ws1, name, ws2, colon, ws3, value, initializer], // omit typeSuffix
name,
value,
typeSuffix,
initializer,
names: value.names,
bind: !!bind,
}

_?:ws Caret?:pin BindingIdentifier:binding BindingTypeSuffix?:typeSuffix Initializer?:initializer ->
let children = [ws, binding, initializer] // omit pin and typeSuffix

# NOTE: ^name is short for property `name: ^name`
_?:ws Caret:pin BindingIdentifier:binding BindingTypeSuffix?:typeSuffix Initializer?:initializer ->
// Note that this has the name but not the value.
// This is what we want when destructuring, but not in function params.
const children = [ws, binding]
// TODO make this work with pin
if (binding.type === "AtBinding") {
children.push({
type: "Error",
message: "Pinned properties do not yet work with @binding",
})
}
if (typeSuffix) {
children.push({
type: "Error",
message: "Pinned properties cannot have type annotations",
})
}
if (initializer) {
children.push({
type: "Error",
message: "Pinned properties cannot have initializers",
})
}
return {
type: "PinProperty",
children,
name: binding,
value: {
type: "PinPattern",
children: [binding],
expression: binding,
},
}

# NOTE: name^ means we should bind name, but we do anyway, so allow but ignore
_?:ws BindingIdentifier:binding Caret?:bind BindingTypeSuffix?:typeSuffix Initializer?:initializer ->
const children = [ws, binding, initializer] // omit bind, typeSuffix

if (binding.type === "AtBinding") {
return {
type: "AtBindingProperty",
Expand All @@ -2201,34 +2238,6 @@ BindingProperty
}
}

if (pin) {
// Note that this has the name but not the value.
// This is what we want when destructuring, but not in function params.
children = [ws, binding]
if (typeSuffix) {
children.push({
type: "Error",
message: "Pinned properties cannot have type annotations",
})
}
if (initializer) {
children.push({
type: "Error",
message: "Pinned properties cannot have initializers",
})
}
return {
type: "PinProperty",
children,
name: binding,
value: {
type: "PinPattern",
children: [binding],
expression: binding,
},
}
}

return {
type: "BindingProperty",
children,
Expand All @@ -2238,6 +2247,7 @@ BindingProperty
initializer,
names: binding.names,
identifier: binding,
bind: !!bind,
}

# https://262.ecma-international.org/#prod-BindingRestProperty
Expand Down
83 changes: 47 additions & 36 deletions source/parser/pattern-matching.civet
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import type {
ContinueStatement
ElseClause
Identifier
ObjectBindingPatternContent
ParenthesizedExpression
ParseRule
PatternClause
PatternExpression
PinProperty
StatementTuple
SwitchStatement
} from ./types.civet
Expand All @@ -28,6 +29,7 @@ import {
isExit
makeLeftHandSideExpression
makeNode
prepend
replaceNode
updateParentPointers
} from ./util.civet
Expand Down Expand Up @@ -57,7 +59,6 @@ import {
import {
ReservedWord
} from ../parser.hera
declare var ReservedWord: ParseRule

function processPatternTest(lhs: ASTNode, patterns: PatternExpression[]): ASTNode
{ ref, refAssignmentComma } := maybeRefAssignment lhs, "m"
Expand Down Expand Up @@ -302,6 +303,9 @@ function getPatternBlockPrefix(
// Gather bindings
[splices, thisAssignments] .= gatherBindingCode(pattern)
patternBindings := nonMatcherBindings(pattern)
subbindings :=
for each p of gatherRecursiveAll patternBindings, (& as BindingProperty).subbinding?
prepend ", ", (p as BindingProperty).subbinding

splices = splices.map (s) => [", ", nonMatcherBindings(s)]
thisAssignments = thisAssignments.map ['', &, ";"]
Expand All @@ -311,7 +315,7 @@ function getPatternBlockPrefix(
[
['', {
type: "Declaration"
children: [decl, patternBindings, typeSuffix, " = ", ref, ...splices]
children: [decl, patternBindings, typeSuffix, " = ", ref, ...subbindings, ...splices]
names: []
bindings: [] // avoid implicit return of any bindings
}, ";"]
Expand All @@ -337,45 +341,52 @@ function elideMatchersFromArrayBindings(elements: ArrayBindingPatternContent): A
c is element.binding ? binding : c
}

function elideMatchersFromPropertyBindings(properties) {
return properties.map((p) => {
switch (p.type) {
case "BindingProperty": {
const { children, name, value } = p
const [ws] = children
function elideMatchersFromPropertyBindings(properties: ObjectBindingPatternContent): ObjectBindingPatternContent
for each p of properties
switch p.type
when "BindingProperty", "PinProperty"
{ children, name, value, bind } := p
[ws] := children

shouldElide := (or)
name.type is "NumericLiteral" and !value?.name
name.type is "ComputedPropertyName" and value?.subtype is "NumericLiteral"
if shouldElide
if bind
type: "Error" as const
message: `Cannot bind ${name.type}`
else
continue
else

return if shouldElide

switch (value and value.type) {
when "ArrayBindingPattern", "ObjectBindingPattern"
bindings := nonMatcherBindings(value)
return {
...p,
children: [ws, name, bindings && ": ", bindings, p.delim],
let contents: (BindingProperty | PinProperty)?
switch value?.type
when "ArrayBindingPattern", "ObjectBindingPattern"
bindings := nonMatcherBindings(value)
contents = {
...p
value: bindings
children: [ws, name, bindings && ": ", bindings, p.delim]
}
when "Identifier", undefined
contents = p
else // "Literal", "RegularExpressionLiteral", "StringLiteral"
contents = undefined
if bind
{
...p
children: [ws, name, p.delim]
subbinding: if contents?.value
. contents.value
. " = "
. name
}
when "Identifier"
return p
case "Literal":
case "RegularExpressionLiteral":
case "StringLiteral":
default:
return {
...p,
children: [ws, name, p.delim],
}
}
}
case "PinProperty":
case "BindingRestProperty":
default:
return p
}
})
}
else if contents
contents
else
continue
else // "BindingRestProperty"
p

function nonMatcherBindings(pattern: ASTNodeObject)
switch pattern.type
Expand Down
4 changes: 3 additions & 1 deletion source/parser/types.civet
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,8 @@ export type BindingProperty =
typeSuffix: TypeSuffix?
initializer: Initializer?
delim: ASTNode
bind?: boolean
subbinding?: ASTNode

export type PinProperty =
type: "PinProperty"
Expand Down Expand Up @@ -906,7 +908,7 @@ export type BindingRestProperty =
names?: string[]

export type ObjectBindingPatternContent =
(BindingProperty | PinProperty | AtBindingProperty | BindingRestProperty)[]
(BindingProperty | PinProperty | AtBindingProperty | BindingRestProperty | ASTError)[]

export type ObjectBindingPattern =
type: "ObjectBindingPattern",
Expand Down
8 changes: 4 additions & 4 deletions test/if.civet
Original file line number Diff line number Diff line change
Expand Up @@ -1119,12 +1119,12 @@ describe "if", ->
testCase """
if declaration with values
---
if {type: "Identifier", name: /^[A-Z]*$/} := node
if {type: "Identifier", name^: /^[A-Z]*$/} := node
console.log "upper case", name
else
console.log "not upper case"
---
if ((node) && typeof node === 'object' && 'type' in node && node.type === "Identifier" && 'name' in node && typeof node.name === 'string' && /^[A-Z]*$/.test(node.name)) {const {type, name} = node;
if ((node) && typeof node === 'object' && 'type' in node && node.type === "Identifier" && 'name' in node && typeof node.name === 'string' && /^[A-Z]*$/.test(node.name)) {const { name} = node;
console.log("upper case", name)
}
else {
Expand Down Expand Up @@ -1381,15 +1381,15 @@ describe "if", ->
if {x, ^y} := obj
console.log x
---
if ((obj) && typeof obj === 'object' && 'x' in obj && 'y' in obj && obj.y === y) {const {x, y} = obj;
if ((obj) && typeof obj === 'object' && 'x' in obj && 'y' in obj && obj.y === y) {const {x,} = obj;
console.log(x)
}
"""

testCase """
duplicate bindings
---
if [{type: "text"}, {type: "image"}] := array
if [{type^: "text"}, {type^: "image"}] := array
console.log type
---
function len<T extends readonly unknown[], N extends number>(arr: T, length: N): arr is T & { length: N } { return arr.length === length }
Expand Down
35 changes: 29 additions & 6 deletions test/switch.civet
Original file line number Diff line number Diff line change
Expand Up @@ -1057,6 +1057,17 @@ describe "switch", ->
{a, b: 3}
console.log a, b
---
if(typeof x === 'object' && x != null && 'a' in x && 'b' in x && x.b === 3) {const {a,} = x;
console.log(a, b)}
"""

testCase """
object pattern with bind and matcher
---
switch x
{a^, b^: 3}
console.log a, b
---
if(typeof x === 'object' && x != null && 'a' in x && 'b' in x && x.b === 3) {const {a, b} = x;
console.log(a, b)}
"""
Expand All @@ -1065,7 +1076,7 @@ describe "switch", ->
object pattern with post rest matcher
---
switch x
{a, b..., c: 3}
{a, b..., c^: 3}
console.log a, b, c
---
if(typeof x === 'object' && x != null && 'a' in x && 'c' in x && x.c === 3) {const {a, c, ...b} = x;
Expand All @@ -1079,7 +1090,7 @@ describe "switch", ->
{a, b: ^b}
console.log a, b
---
if(typeof x === 'object' && x != null && 'a' in x && 'b' in x && x.b === b) {const {a, b} = x;
if(typeof x === 'object' && x != null && 'a' in x && 'b' in x && x.b === b) {const {a,} = x;
console.log(a, b)}
"""

Expand All @@ -1090,7 +1101,7 @@ describe "switch", ->
{a, ^b}
console.log a, b
---
if(typeof x === 'object' && x != null && 'a' in x && 'b' in x && x.b === b) {const {a, b} = x;
if(typeof x === 'object' && x != null && 'a' in x && 'b' in x && x.b === b) {const {a,} = x;
console.log(a, b)}
"""

Expand All @@ -1106,6 +1117,18 @@ describe "switch", ->
console.log(a, c, d)}
"""

testCase """
object pattern with bind and array binding match
---
switch x
{a, b^: [c, d]}
console.log a, c, d
---
function len<T extends readonly unknown[], N extends number>(arr: T, length: N): arr is T & { length: N } { return arr.length === length }
if(typeof x === 'object' && x != null && 'a' in x && 'b' in x && Array.isArray(x.b) && len(x.b, 2)) {const {a, b} = x, [c, d] = b;
console.log(a, c, d)}
"""

testCase """
object pattern with computed property
---
Expand Down Expand Up @@ -1467,7 +1490,7 @@ describe "switch", ->
duplicate bindings
---
switch x
[{type: "text"}, {type: "image"}]
[{type^: "text"}, {type^: "image"}]
x
---
function len<T extends readonly unknown[], N extends number>(arr: T, length: N): arr is T & { length: N } { return arr.length === length }
Expand All @@ -1490,7 +1513,7 @@ describe "switch", ->
duplicate bindings with rest
---
switch x
[{type: "text"}, ..., {type: "image"}]
[{type^: "text"}, ..., {type^: "image"}]
x
---
if(Array.isArray(x) && x.length >= 2 && typeof x[0] === 'object' && x[0] != null && 'type' in x[0] && x[0].type === "text" && typeof x[x.length - 1] === 'object' && x[x.length - 1] != null && 'type' in x[x.length - 1] && x[x.length - 1].type === "image") {const [{type: type1}, ...ref] = x, [{type: type2}] = ref.splice(-1);const type = [type1, type2];
Expand Down Expand Up @@ -1547,7 +1570,7 @@ describe "switch", ->
aliased duplicate bindings
---
switch data
{a: 'z', a: 'x'}
{a^: 'z', a^: 'x'}
a
---
if(typeof data === 'object' && data != null && 'a' in data && data.a === 'z' && 'a' in data && data.a === 'x') {const {a: a1, a: a2} = data;const a = [a1, a2];
Expand Down
Loading

0 comments on commit 246bab8

Please sign in to comment.