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

Pattern name^: value binds name, while name: value never does (except for identifiers) #1663

Merged
merged 2 commits into from
Dec 27, 2024
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
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 @@ -2175,21 +2175,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 @@ -2202,34 +2239,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 @@ -2239,6 +2248,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 @@ -871,6 +871,8 @@ export type BindingProperty =
typeSuffix: TypeSuffix?
initializer: Initializer?
delim: ASTNode
bind?: boolean
subbinding?: ASTNode

export type PinProperty =
type: "PinProperty"
Expand Down Expand Up @@ -905,7 +907,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
Loading