From e2a0975af0fbfb5b040aceec3d1b878f44eb6a70 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Thu, 2 Jan 2025 12:50:54 -0500 Subject: [PATCH 01/17] `name^pattern` named binding pattern --- source/parser.hera | 28 +++++++++++++++++++------- source/parser/pattern-matching.civet | 30 +++++++++++++++++++++++----- source/parser/types.civet | 19 ++++++++++++++---- test/switch.civet | 11 ++++++++++ test/try.civet | 22 ++++++++++++++++++++ 5 files changed, 94 insertions(+), 16 deletions(-) diff --git a/source/parser.hera b/source/parser.hera index 1a8a8357..05651d88 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -2067,6 +2067,19 @@ PinPattern expression, } +# `name ^ pattern` means bind the whole thing to `name`, +# but also destructure/match `pattern` +NamedBindingPattern + Identifier:name _?:ws1 Caret _?:ws2 BindingPattern:pattern -> + name = append(name, ws1) + pattern = prepend(ws2, pattern) + return { + type: "NamedBindingPattern", + children: [name, pattern], + name, + pattern, + } + # https://262.ecma-international.org/#prod-BindingPattern BindingPattern ObjectBindingPattern @@ -2074,6 +2087,7 @@ BindingPattern PinPattern Literal RegularExpressionLiteral + NamedBindingPattern # https://262.ecma-international.org/#prod-ObjectBindingPattern # NOTE: Simplified from spec @@ -5220,13 +5234,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", @@ -5240,6 +5247,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 diff --git a/source/parser/pattern-matching.civet b/source/parser/pattern-matching.civet index 229bca16..b2ea381f 100644 --- a/source/parser/pattern-matching.civet +++ b/source/parser/pattern-matching.civet @@ -10,6 +10,7 @@ import type { Condition ContinueStatement ElseClause + HasSubbinding Identifier ObjectBindingPatternContent ParenthesizedExpression @@ -277,6 +278,9 @@ function getPatternConditions( pattern.expression, ]) + when "NamedBindingPattern" + getPatternConditions pattern.pattern, ref, conditions + when "Literal" conditions.push([ ref, @@ -303,9 +307,16 @@ 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 + + // Find all `subbinding` properties, including search within those + // subbindings for more subbindings + subbindings: ASTNode[] := [] + function findSubbindings(node: ASTNode): void + for each p of gatherRecursiveAll node, ($): $ is HasSubbinding => ($ as HasSubbinding).subbinding? + { subbinding } := p + subbindings.push prepend ", ", subbinding + findSubbindings subbinding + findSubbindings patternBindings splices = splices.map (s) => [", ", nonMatcherBindings(s)] thisAssignments = thisAssignments.map ['', &, ";"] @@ -323,7 +334,7 @@ function getPatternBlockPrefix( ...duplicateDeclarations.map ['', &, ";"] ] -function elideMatchersFromArrayBindings(elements: ArrayBindingPatternContent): ASTNode[] +function elideMatchersFromArrayBindings(elements: ArrayBindingPatternContent): ArrayBindingPatternContent for each element of elements switch element.type when "BindingRestElement", "ElisionElement" @@ -388,7 +399,7 @@ function elideMatchersFromPropertyBindings(properties: ObjectBindingPatternConte else // "BindingRestProperty" p -function nonMatcherBindings(pattern: ASTNodeObject) +function nonMatcherBindings(pattern: ASTNodeObject): ASTNodeObject switch pattern.type when "ArrayBindingPattern", "PostRestBindingElements" elements := elideMatchersFromArrayBindings pattern.elements @@ -404,6 +415,15 @@ function nonMatcherBindings(pattern: ASTNodeObject) properties children: pattern.children.map & is pattern.properties ? properties : & } + when "NamedBindingPattern" + makeNode { + ...pattern + children: [ pattern.name ] + subbinding: + . nonMatcherBindings pattern.pattern + . " = " + . pattern.name + } else pattern diff --git a/source/parser/types.civet b/source/parser/types.civet index d2cdd7b1..deb3a913 100644 --- a/source/parser/types.civet +++ b/source/parser/types.civet @@ -110,6 +110,7 @@ export type OtherNode = | Initializer | Label | ModuleSpecifier + | NamedBindingPattern | NonNullAssertion | NormalCatchParameter | ObjectBindingPattern @@ -758,7 +759,7 @@ export type FinallyToken = "finally " | { $loc: Loc, token: "finally" } export type BindingIdentifier = AtBinding | Identifier | ReturnValue -export type BindingPattern = BindingIdentifier | ObjectBindingPattern | ArrayBindingPattern | PinPattern | Literal | RegularExpressionLiteral +export type BindingPattern = BindingIdentifier | ObjectBindingPattern | ArrayBindingPattern | PinPattern | NamedBindingPattern | Literal | RegularExpressionLiteral // Includes things that appear inside BindingPatterns (when recursing) // but aren't patterns by themselves @@ -827,27 +828,37 @@ export type EmptyBinding names: string[] ref: ASTRef -export type ElisionElement = +export type ElisionElement type: "ElisionElement" children: Children parent?: Parent typeSuffix?: undefined names: string[] -export type Placeholder = +export type Placeholder type: "Placeholder" subtype: "." | "&" children: Children & [ASTLeaf] parent?: Parent typeSuffix?: TypeSuffix? -export type PinPattern = +export type PinPattern type: "PinPattern" children: Children parent?: Parent expression: ExpressionNode ref?: ASTRef +export type NamedBindingPattern + type: "NamedBindingPattern" + children: Children + parent?: Parent + name: Identifier + pattern: BindingPattern + subbinding?: ASTNode + +export type HasSubbinding = BindingProperty | NamedBindingPattern + // _?, __ export type Whitespace = (ASTLeaf | ASTString)[]? diff --git a/test/switch.civet b/test/switch.civet index a3bcfe93..3f15901c 100644 --- a/test/switch.civet +++ b/test/switch.civet @@ -1678,6 +1678,17 @@ describe "switch", -> }} """ + testCase """ + named object + --- + switch x + o^{a, b^: {c}} + console.log o, a, b, c + --- + if(typeof x === 'object' && x != null && 'a' in x && 'b' in x && typeof x.b === 'object' && x.b != null && 'c' in x.b) {const o = x, {a, b} = o, {c} = b; + console.log(o, a, b, c)} + """ + describe "continue switch", -> // NOTE: newline escapes prevent trim trailing whitespace editor config from messing with the test formatting testCase """ diff --git a/test/try.civet b/test/try.civet index 105dc17d..c03072b8 100644 --- a/test/try.civet +++ b/test/try.civet @@ -376,6 +376,28 @@ describe "try", -> ParseErrors: unknown:3:10 Only one catch clause allowed unless using pattern matching """ + testCase """ + named + --- + try + foo() + catch e^{name: "RangeError"} + console.log 'RangeError!' + catch e ^ {name: "ReferenceError", message} + console.log 'ReferenceError!', message + --- + try { + foo() + } + catch(e1) {if(typeof e1 === 'object' && e1 != null && 'name' in e1 && e1.name === "RangeError") {const e = e1, {} = e; + console.log('RangeError!') + } + else if(typeof e1 === 'object' && e1 != null && 'name' in e1 && e1.name === "ReferenceError" && 'message' in e1) {const e = e1, { message} = e ; + console.log('ReferenceError!', message) + } + else {throw e1}} + """ + describe "lone finally blocks", -> testCase """ basic From 23645e8eb612c9ef00139b9ac7b43655e31bc501 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Thu, 2 Jan 2025 13:00:54 -0500 Subject: [PATCH 02/17] `@x^pattern` --- source/parser.hera | 8 ++++---- source/parser/pattern-matching.civet | 4 ++-- source/parser/types.civet | 2 +- test/switch.civet | 4 ++++ 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/source/parser.hera b/source/parser.hera index 05651d88..3c86e267 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -2070,13 +2070,13 @@ PinPattern # `name ^ pattern` means bind the whole thing to `name`, # but also destructure/match `pattern` NamedBindingPattern - Identifier:name _?:ws1 Caret _?:ws2 BindingPattern:pattern -> - name = append(name, ws1) + BindingIdentifier:binding _?:ws1 Caret _?:ws2 BindingPattern:pattern -> + binding = append(binding, ws1) pattern = prepend(ws2, pattern) return { type: "NamedBindingPattern", - children: [name, pattern], - name, + children: [binding, pattern], + binding, pattern, } diff --git a/source/parser/pattern-matching.civet b/source/parser/pattern-matching.civet index b2ea381f..bc947e45 100644 --- a/source/parser/pattern-matching.civet +++ b/source/parser/pattern-matching.civet @@ -418,11 +418,11 @@ function nonMatcherBindings(pattern: ASTNodeObject): ASTNodeObject when "NamedBindingPattern" makeNode { ...pattern - children: [ pattern.name ] + children: [ pattern.binding ] subbinding: . nonMatcherBindings pattern.pattern . " = " - . pattern.name + . pattern.binding } else pattern diff --git a/source/parser/types.civet b/source/parser/types.civet index deb3a913..49e2c011 100644 --- a/source/parser/types.civet +++ b/source/parser/types.civet @@ -853,7 +853,7 @@ export type NamedBindingPattern type: "NamedBindingPattern" children: Children parent?: Parent - name: Identifier + binding: BindingIdentifier pattern: BindingPattern subbinding?: ASTNode diff --git a/test/switch.civet b/test/switch.civet index 3f15901c..119c6d53 100644 --- a/test/switch.civet +++ b/test/switch.civet @@ -1684,9 +1684,13 @@ describe "switch", -> switch x o^{a, b^: {c}} console.log o, a, b, c + @x^{a} + console.log @x, a --- if(typeof x === 'object' && x != null && 'a' in x && 'b' in x && typeof x.b === 'object' && x.b != null && 'c' in x.b) {const o = x, {a, b} = o, {c} = b; console.log(o, a, b, c)} + else if(typeof x === 'object' && x != null && 'a' in x) {const x1 = x, {a} = x1;this.x = x1; + console.log(this.x, a)} """ describe "continue switch", -> From 4447da22be4d3dca3e46225383869089e37ed5b7 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Thu, 2 Jan 2025 13:10:31 -0500 Subject: [PATCH 03/17] Forbid space after `^` in `name^` --- source/parser.hera | 7 +++---- test/try.civet | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/source/parser.hera b/source/parser.hera index 3c86e267..c53feb84 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -2067,12 +2067,11 @@ PinPattern expression, } -# `name ^ pattern` means bind the whole thing to `name`, +# `name^ pattern` means bind the whole thing to `name`, # but also destructure/match `pattern` NamedBindingPattern - BindingIdentifier:binding _?:ws1 Caret _?:ws2 BindingPattern:pattern -> - binding = append(binding, ws1) - pattern = prepend(ws2, pattern) + BindingIdentifier:binding Caret _?:ws BindingPattern:pattern -> + pattern = prepend(ws, pattern) return { type: "NamedBindingPattern", children: [binding, pattern], diff --git a/test/try.civet b/test/try.civet index c03072b8..e2dbcc4f 100644 --- a/test/try.civet +++ b/test/try.civet @@ -383,7 +383,7 @@ describe "try", -> foo() catch e^{name: "RangeError"} console.log 'RangeError!' - catch e ^ {name: "ReferenceError", message} + catch e^ {name: "ReferenceError", message} console.log 'ReferenceError!', message --- try { @@ -392,7 +392,7 @@ describe "try", -> catch(e1) {if(typeof e1 === 'object' && e1 != null && 'name' in e1 && e1.name === "RangeError") {const e = e1, {} = e; console.log('RangeError!') } - else if(typeof e1 === 'object' && e1 != null && 'name' in e1 && e1.name === "ReferenceError" && 'message' in e1) {const e = e1, { message} = e ; + else if(typeof e1 === 'object' && e1 != null && 'name' in e1 && e1.name === "ReferenceError" && 'message' in e1) {const e = e1, { message} = e; console.log('ReferenceError!', message) } else {throw e1}} From ab49233628b120b99251f1b836d18e4c8b79454d Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Thu, 2 Jan 2025 13:11:05 -0500 Subject: [PATCH 04/17] Allow identifier before condition fragment pattern --- source/parser.hera | 11 ++++++++--- source/parser/pattern-matching.civet | 23 ++++++++++++----------- source/parser/types.civet | 4 ++++ test/switch.civet | 4 ++++ test/try.civet | 16 ++++++++-------- 5 files changed, 36 insertions(+), 22 deletions(-) diff --git a/source/parser.hera b/source/parser.hera index c53feb84..794ebaed 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -5145,12 +5145,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 diff --git a/source/parser/pattern-matching.civet b/source/parser/pattern-matching.civet index bc947e45..b16f15db 100644 --- a/source/parser/pattern-matching.civet +++ b/source/parser/pattern-matching.civet @@ -252,18 +252,15 @@ function getPatternConditions( getPatternConditions(value, subRef, conditions) if value when "ConditionFragment" - let { children } = pattern + { rhs } .= pattern // Add leading space to first binary operation - if (children.length) { - let [ first, ...rest ] = children - let [ ws, ...op ] = first - ws = [" "].concat(ws) + if rhs# + [ first, ...rest ] .= rhs + [ ws, ...op ] .= first + ws = [" "].concat ws first = [ ws, ...op ] - children = [ first, ...rest ] - } - conditions.push( - processBinaryOpExpression([ref, children]) - ) + rhs = [ first, ...rest ] + conditions.push processBinaryOpExpression [ref, rhs] when "RegularExpressionLiteral" conditions.push( @@ -301,8 +298,10 @@ function getPatternBlockPrefix( return unless pattern.length when "ObjectBindingPattern" return unless pattern.properties.length - when "Literal", "RegularExpressionLiteral", "PinPattern", "ConditionFragment" + when "Literal", "RegularExpressionLiteral", "PinPattern" return + when "ConditionFragment" + return unless pattern.binding // Gather bindings [splices, thisAssignments] .= gatherBindingCode(pattern) @@ -424,6 +423,8 @@ function nonMatcherBindings(pattern: ASTNodeObject): ASTNodeObject . " = " . pattern.binding } + when "ConditionFragment" + pattern.binding else pattern diff --git a/source/parser/types.civet b/source/parser/types.civet index 49e2c011..165b5096 100644 --- a/source/parser/types.civet +++ b/source/parser/types.civet @@ -220,6 +220,8 @@ export type BinaryOp = (string & relational?: never ) +export type BinaryOpRHS = [ASTNode, BinaryOp, ASTNode, ASTNode] + export type NonNullAssertion type: "NonNullAssertion" ts: true @@ -771,6 +773,8 @@ export type ConditionFragment = type: "ConditionFragment" children: Children parent?: Parent + rhs: BinaryOpRHS[] + binding?: BindingPattern? export type RegularExpressionLiteral = type: "RegularExpressionLiteral" diff --git a/test/switch.civet b/test/switch.civet index 119c6d53..2bb2774f 100644 --- a/test/switch.civet +++ b/test/switch.civet @@ -1686,11 +1686,15 @@ describe "switch", -> console.log o, a, b, c @x^{a} console.log @x, a + @x is 5 + console.log @x --- if(typeof x === 'object' && x != null && 'a' in x && 'b' in x && typeof x.b === 'object' && x.b != null && 'c' in x.b) {const o = x, {a, b} = o, {c} = b; console.log(o, a, b, c)} else if(typeof x === 'object' && x != null && 'a' in x) {const x1 = x, {a} = x1;this.x = x1; console.log(this.x, a)} + else if(x === 5) {const x2 = x;this.x = x2; + console.log(this.x)} """ describe "continue switch", -> diff --git a/test/try.civet b/test/try.civet index e2dbcc4f..ad709d60 100644 --- a/test/try.civet +++ b/test/try.civet @@ -381,19 +381,19 @@ describe "try", -> --- try foo() - catch e^{name: "RangeError"} - console.log 'RangeError!' - catch e^ {name: "ReferenceError", message} - console.log 'ReferenceError!', message + catch e Date: Thu, 2 Jan 2025 23:03:18 -0500 Subject: [PATCH 05/17] Named destructured parameters in functions --- source/parser.hera | 21 ++++++++---- source/parser/binding.civet | 13 ++++++++ source/parser/function.civet | 13 ++++++-- source/parser/op.civet | 1 - source/parser/pattern-matching.civet | 15 ++------- test/function.civet | 49 ++++++++++++++++++++++++++++ 6 files changed, 90 insertions(+), 22 deletions(-) diff --git a/source/parser.hera b/source/parser.hera index 794ebaed..0483d523 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -1983,7 +1983,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", @@ -2074,9 +2074,12 @@ NamedBindingPattern pattern = prepend(ws, pattern) return { type: "NamedBindingPattern", - children: [binding, pattern], + // 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 @@ -2188,7 +2191,7 @@ 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 -> return { type: "BindingProperty", children: [ws1, name, ws2, colon, ws3, value, initializer], // omit typeSuffix @@ -2305,7 +2308,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, @@ -2325,7 +2328,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], @@ -2337,7 +2340,7 @@ BindingRestElement rest: true, } - _?:ws ( BindingIdentifier / BindingPattern ):binding DotDotDot:dots -> + _?:ws ( BindingPattern / BindingIdentifier ):binding DotDotDot:dots -> return { type: "BindingRestElement", children: [...(ws || []), dots, binding], @@ -5006,6 +5009,12 @@ 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], diff --git a/source/parser/binding.civet b/source/parser/binding.civet index c8714c40..1137ab0a 100644 --- a/source/parser/binding.civet +++ b/source/parser/binding.civet @@ -5,6 +5,7 @@ import type { BindingPattern BindingRestElement Children + HasSubbinding ObjectBindingPattern ThisAssignments } from ./types.civet @@ -107,6 +108,17 @@ function adjustBindingElements(elements: ASTNodeObject[]) length } +/** +Find and return all `subbinding` properties, prefixed with commas, +including searching within those subbindings for more subbindings +*/ +function findSubbindings(node: ASTNode, subbindings: ASTNode[] = []): ASTNode[] + for each p of gatherRecursiveAll node, ($): $ is HasSubbinding => ($ as HasSubbinding).subbinding? + { subbinding } := p + subbindings.push ", ", subbinding + findSubbindings subbinding, subbindings + subbindings + function gatherBindingCode(statements: ASTNode, opts?: { injectParamProps?: boolean, assignPins?: boolean }) thisAssignments: ThisAssignments := [] splices: unknown[] := [] @@ -234,6 +246,7 @@ function gatherBindingPatternTypeSuffix(pattern: ArrayBindingPattern | ObjectBin export { adjustAtBindings adjustBindingElements + findSubbindings gatherBindingCode gatherBindingPatternTypeSuffix } diff --git a/source/parser/function.civet b/source/parser/function.civet index 4be7725c..db7593d3 100644 --- a/source/parser/function.civet +++ b/source/parser/function.civet @@ -44,6 +44,7 @@ import { } from ./block.civet import { + findSubbindings gatherBindingCode gatherBindingPatternTypeSuffix } from ./binding.civet @@ -900,8 +901,7 @@ function processParams(f: FunctionNode): void rest.children.pop() // remove delimiter if after# // non-end rest - if rest.binding.type is "ArrayBindingPattern" or - rest.binding.type is "ObjectBindingPattern" + if rest.binding.type is like "ArrayBindingPattern", "ObjectBindingPattern", "NamedBindingPattern" parameters.parameters.push type: "Error" message: "Non-end rest parameter cannot be binding pattern" @@ -1011,6 +1011,7 @@ function processParams(f: FunctionNode): void [splices, thisAssignments] := gatherBindingCode parameters, injectParamProps: isConstructor assignPins: true + subbindings := findSubbindings parameters.parameters // `@(@x: number)` adds `x: number` declaration to class body if isConstructor @@ -1057,6 +1058,14 @@ function processParams(f: FunctionNode): void children: [";"] prefix: ASTNode[] .= [] + if subbindings# + prefix.push makeNode { + type: "Declaration" + children: ["const ", subbindings[1..]] + names: subbindings.flatMap .names ?? [] + bindings: [] + decl: "const" + } satisfies Declaration for each binding of splices as Binding[] assert.equal binding.type, "PostRestBindingElements", "splice should be of type Binding" prefix.push makeNode { diff --git a/source/parser/op.civet b/source/parser/op.civet index 1725da1b..ebbca69b 100644 --- a/source/parser/op.civet +++ b/source/parser/op.civet @@ -6,7 +6,6 @@ import type { } from ./types.civet import { - assert makeLeftHandSideExpression replaceNode trimFirstSpace diff --git a/source/parser/pattern-matching.civet b/source/parser/pattern-matching.civet index b16f15db..40c72d7f 100644 --- a/source/parser/pattern-matching.civet +++ b/source/parser/pattern-matching.civet @@ -10,7 +10,6 @@ import type { Condition ContinueStatement ElseClause - HasSubbinding Identifier ObjectBindingPatternContent ParenthesizedExpression @@ -30,7 +29,6 @@ import { isExit makeLeftHandSideExpression makeNode - prepend replaceNode updateParentPointers } from ./util.civet @@ -46,6 +44,7 @@ import { } from ./block.civet import { + findSubbindings gatherBindingCode } from ./binding.civet @@ -306,16 +305,7 @@ function getPatternBlockPrefix( // Gather bindings [splices, thisAssignments] .= gatherBindingCode(pattern) patternBindings := nonMatcherBindings(pattern) - - // Find all `subbinding` properties, including search within those - // subbindings for more subbindings - subbindings: ASTNode[] := [] - function findSubbindings(node: ASTNode): void - for each p of gatherRecursiveAll node, ($): $ is HasSubbinding => ($ as HasSubbinding).subbinding? - { subbinding } := p - subbindings.push prepend ", ", subbinding - findSubbindings subbinding - findSubbindings patternBindings + subbindings := findSubbindings patternBindings splices = splices.map (s) => [", ", nonMatcherBindings(s)] thisAssignments = thisAssignments.map ['', &, ";"] @@ -417,7 +407,6 @@ function nonMatcherBindings(pattern: ASTNodeObject): ASTNodeObject when "NamedBindingPattern" makeNode { ...pattern - children: [ pattern.binding ] subbinding: . nonMatcherBindings pattern.pattern . " = " diff --git a/test/function.civet b/test/function.civet index 6dcc0298..5484d6cd 100644 --- a/test/function.civet +++ b/test/function.civet @@ -479,6 +479,55 @@ describe "function", -> (a1) => {a = a1;return 1} """ + describe "named destructured parameters", -> + testCase """ + named object + --- + function Comp(props^{x, y}) + --- + function Comp(props){const {x, y} = props;} + """ + + testCase """ + named object with type + --- + function Comp(props^{x, y}: {x: number, y: number}) + --- + function Comp(props: {x: number, y: number}){const {x, y} = props;} + """ + + testCase """ + named object with subtypes + --- + function Comp(props^{x:: number, y:: number}) + --- + function Comp(props: {x: number, y: number}){const {x, y} = props;} + """ + + testCase """ + named array + --- + function f(array^[first, ...]) + --- + function f(array){const [first, ...ref] = array;} + """ + + testCase """ + object inside array + --- + function f(array^[first^{x, y}, ...]) + --- + function f(array){const [first, ...ref] = array, {x, y} = first;} + """ + + testCase """ + array inside object + --- + function Comp(props^{array: array^[first, ...]}) + --- + function Comp(props){const {array: array} = props, [first, ...ref] = array;} + """ + testCase """ longhand --- From 31600b3cfbf93cacfe5edcc0c581986724bbd888 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Fri, 3 Jan 2025 11:30:21 -0500 Subject: [PATCH 06/17] Restructure `{name^: pattern}` into `{name: name^pattern}` --- source/parser.hera | 16 +++++++++++-- source/parser/pattern-matching.civet | 35 ++++++++++++++-------------- source/parser/types.civet | 1 - test/switch.civet | 4 ++-- 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/source/parser.hera b/source/parser.hera index 0483d523..5912f49f 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -2192,6 +2192,20 @@ 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 ( 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 @@ -2200,7 +2214,6 @@ BindingProperty typeSuffix, initializer, names: value.names, - bind: !!bind, } # NOTE: ^name is short for property `name: ^name` @@ -2263,7 +2276,6 @@ BindingProperty initializer, names: binding.names, identifier: binding, - bind: !!bind, } # https://262.ecma-international.org/#prod-BindingRestProperty diff --git a/source/parser/pattern-matching.civet b/source/parser/pattern-matching.civet index 40c72d7f..a1a8af5c 100644 --- a/source/parser/pattern-matching.civet +++ b/source/parser/pattern-matching.civet @@ -345,24 +345,20 @@ function elideMatchersFromPropertyBindings(properties: ObjectBindingPatternConte for each p of properties switch p.type when "BindingProperty", "PinProperty" - { children, name, value, bind } := p + { children, name, value } := 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 + continue else let contents: (BindingProperty | PinProperty)? switch value?.type when "ArrayBindingPattern", "ObjectBindingPattern" - bindings := nonMatcherBindings(value) + bindings := nonMatcherBindings value contents = { ...p value: bindings @@ -370,18 +366,23 @@ function elideMatchersFromPropertyBindings(properties: ObjectBindingPatternConte } when "Identifier", undefined contents = p + when "NamedBindingPattern" + bindings := nonMatcherBindings value.pattern + contents = { + ...p + subbinding: if bindings.type is like "ArrayBindingPattern", "ObjectBindingPattern", "Identifier" + . bindings + . " = " + . name + } + // Simplify `{name: name}` to just `{name}` + // This occurs because the parser expands `{name^: pattern}` + // into `{name: name^pattern}` + if p.name is value.binding + contents!.children = [ws, name, p.delim] else // "Literal", "RegularExpressionLiteral", "StringLiteral" contents = undefined - if bind - { - ...p - children: [ws, name, p.delim] - subbinding: if contents?.value - . contents.value - . " = " - . name - } - else if contents + if contents contents else continue diff --git a/source/parser/types.civet b/source/parser/types.civet index 165b5096..e710703b 100644 --- a/source/parser/types.civet +++ b/source/parser/types.civet @@ -887,7 +887,6 @@ export type BindingProperty = typeSuffix: TypeSuffix? initializer: Initializer? delim: ASTNode - bind?: boolean subbinding?: ASTNode export type PinProperty = diff --git a/test/switch.civet b/test/switch.civet index 2bb2774f..473db550 100644 --- a/test/switch.civet +++ b/test/switch.civet @@ -1682,14 +1682,14 @@ describe "switch", -> named object --- switch x - o^{a, b^: {c}} + o^{a, b^: {c, d: /r/}} console.log o, a, b, c @x^{a} console.log @x, a @x is 5 console.log @x --- - if(typeof x === 'object' && x != null && 'a' in x && 'b' in x && typeof x.b === 'object' && x.b != null && 'c' in x.b) {const o = x, {a, b} = o, {c} = b; + if(typeof x === 'object' && x != null && 'a' in x && 'b' in x && typeof x.b === 'object' && x.b != null && 'c' in x.b && 'd' in x.b && typeof x.b.d === 'string' && /r/.test(x.b.d)) {const o = x, {a, b} = o, {c,} = b; console.log(o, a, b, c)} else if(typeof x === 'object' && x != null && 'a' in x) {const x1 = x, {a} = x1;this.x = x1; console.log(this.x, a)} From 51532ec2698f4e55ce99fe09a1c89f94ad44ca74 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Fri, 3 Jan 2025 11:50:30 -0500 Subject: [PATCH 07/17] Named properties work in functions --- source/parser/binding.civet | 13 +++++++++++++ source/parser/function.civet | 3 +++ source/parser/pattern-matching.civet | 3 ++- source/parser/types.civet | 2 +- test/function.civet | 10 ++++++++++ 5 files changed, 29 insertions(+), 2 deletions(-) diff --git a/source/parser/binding.civet b/source/parser/binding.civet index 1137ab0a..28b92e09 100644 --- a/source/parser/binding.civet +++ b/source/parser/binding.civet @@ -3,6 +3,7 @@ import type { ASTNode ASTNodeObject BindingPattern + BindingProperty BindingRestElement Children HasSubbinding @@ -119,6 +120,17 @@ function findSubbindings(node: ASTNode, subbindings: ASTNode[] = []): ASTNode[] findSubbindings 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[] := [] @@ -249,4 +261,5 @@ export { findSubbindings gatherBindingCode gatherBindingPatternTypeSuffix + simplifyBindingProperties } diff --git a/source/parser/function.civet b/source/parser/function.civet index db7593d3..f5c71e0f 100644 --- a/source/parser/function.civet +++ b/source/parser/function.civet @@ -47,6 +47,7 @@ import { findSubbindings gatherBindingCode gatherBindingPatternTypeSuffix + simplifyBindingProperties } from ./binding.civet import { @@ -1012,6 +1013,8 @@ function processParams(f: FunctionNode): void injectParamProps: isConstructor assignPins: true subbindings := findSubbindings parameters.parameters + simplifyBindingProperties parameters.parameters + simplifyBindingProperties subbindings // `@(@x: number)` adds `x: number` declaration to class body if isConstructor diff --git a/source/parser/pattern-matching.civet b/source/parser/pattern-matching.civet index a1a8af5c..3491cc96 100644 --- a/source/parser/pattern-matching.civet +++ b/source/parser/pattern-matching.civet @@ -375,7 +375,8 @@ function elideMatchersFromPropertyBindings(properties: ObjectBindingPatternConte . " = " . name } - // Simplify `{name: name}` to just `{name}` + // Similar to `simplifyBindingProperties`, + // simplify `{name: name}` to just `{name}` // This occurs because the parser expands `{name^: pattern}` // into `{name: name^pattern}` if p.name is value.binding diff --git a/source/parser/types.civet b/source/parser/types.civet index e710703b..82148123 100644 --- a/source/parser/types.civet +++ b/source/parser/types.civet @@ -883,7 +883,7 @@ export type BindingProperty = parent?: Parent name: PropertyName | AtBinding names: string[] - value: BindingIdentifier | BindingPattern + value: (BindingIdentifier | BindingPattern)? typeSuffix: TypeSuffix? initializer: Initializer? delim: ASTNode diff --git a/test/function.civet b/test/function.civet index 5484d6cd..d203ff19 100644 --- a/test/function.civet +++ b/test/function.civet @@ -528,6 +528,16 @@ describe "function", -> function Comp(props){const {array: array} = props, [first, ...ref] = array;} """ + testCase """ + named properties of object + --- + function Comp1(props^{array^: [lead, ...], x^, name^: {first, last, suffix^: {jr}}}) + function Comp2({array^: [lead, ...], x^, name^: {first, last, suffix^: {jr}}}) + --- + function Comp1(props){const {array, x, name} = props, [lead, ...ref] = array, {first, last, suffix} = name, {jr} = suffix;} + function Comp2({array, x, name}){const [lead, ...ref1] = array, {first, last, suffix} = name, {jr} = suffix;} + """ + testCase """ longhand --- From 0ae07bf3fef4649f8f58b25b103f7bc8e009fd64 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Fri, 3 Jan 2025 12:00:03 -0500 Subject: [PATCH 08/17] Named destructuring in `:=`/`.=` declarations --- source/parser/binding.civet | 6 +++--- source/parser/declaration.civet | 7 ++++++- source/parser/function.civet | 4 ++-- source/parser/pattern-matching.civet | 4 ++-- test/assignment.civet | 14 ++++++++++++++ 5 files changed, 27 insertions(+), 8 deletions(-) diff --git a/source/parser/binding.civet b/source/parser/binding.civet index 28b92e09..feda4c5d 100644 --- a/source/parser/binding.civet +++ b/source/parser/binding.civet @@ -113,11 +113,11 @@ function adjustBindingElements(elements: ASTNodeObject[]) Find and return all `subbinding` properties, prefixed with commas, including searching within those subbindings for more subbindings */ -function findSubbindings(node: ASTNode, subbindings: ASTNode[] = []): ASTNode[] +function gatherSubbindings(node: ASTNode, subbindings: ASTNode[] = []): ASTNode[] for each p of gatherRecursiveAll node, ($): $ is HasSubbinding => ($ as HasSubbinding).subbinding? { subbinding } := p subbindings.push ", ", subbinding - findSubbindings subbinding, subbindings + gatherSubbindings subbinding, subbindings subbindings /** @@ -258,7 +258,7 @@ function gatherBindingPatternTypeSuffix(pattern: ArrayBindingPattern | ObjectBin export { adjustAtBindings adjustBindingElements - findSubbindings + gatherSubbindings gatherBindingCode gatherBindingPatternTypeSuffix simplifyBindingProperties diff --git a/source/parser/declaration.civet b/source/parser/declaration.civet index bbe8adff..55b9ffb1 100644 --- a/source/parser/declaration.civet +++ b/source/parser/declaration.civet @@ -60,6 +60,8 @@ import { import { gatherBindingCode + gatherSubbindings + simplifyBindingProperties } from ./binding.civet import { @@ -77,6 +79,9 @@ function processAssignmentDeclaration(decl: ASTLeaf, pattern: Binding["pattern"] } [splices, assignments] .= gatherBindingCode pattern + subbindings := gatherSubbindings pattern + simplifyBindingProperties pattern + simplifyBindingProperties subbindings splices = splices.map (s) => [", ", s] thisAssignments := assignments.map (a) => ["", a, ";"] as const @@ -96,7 +101,7 @@ function processAssignmentDeclaration(decl: ASTLeaf, pattern: Binding["pattern"] children: [pattern, typeSuffix, initializer] } - children := [decl, binding] + children := [decl, binding, subbindings] makeNode { type: "Declaration" diff --git a/source/parser/function.civet b/source/parser/function.civet index f5c71e0f..22809cb9 100644 --- a/source/parser/function.civet +++ b/source/parser/function.civet @@ -44,7 +44,7 @@ import { } from ./block.civet import { - findSubbindings + gatherSubbindings gatherBindingCode gatherBindingPatternTypeSuffix simplifyBindingProperties @@ -1012,7 +1012,7 @@ function processParams(f: FunctionNode): void [splices, thisAssignments] := gatherBindingCode parameters, injectParamProps: isConstructor assignPins: true - subbindings := findSubbindings parameters.parameters + subbindings := gatherSubbindings parameters.parameters simplifyBindingProperties parameters.parameters simplifyBindingProperties subbindings diff --git a/source/parser/pattern-matching.civet b/source/parser/pattern-matching.civet index 3491cc96..ccd792ea 100644 --- a/source/parser/pattern-matching.civet +++ b/source/parser/pattern-matching.civet @@ -44,7 +44,7 @@ import { } from ./block.civet import { - findSubbindings + gatherSubbindings gatherBindingCode } from ./binding.civet @@ -305,7 +305,7 @@ function getPatternBlockPrefix( // Gather bindings [splices, thisAssignments] .= gatherBindingCode(pattern) patternBindings := nonMatcherBindings(pattern) - subbindings := findSubbindings patternBindings + subbindings := gatherSubbindings patternBindings splices = splices.map (s) => [", ", nonMatcherBindings(s)] thisAssignments = thisAssignments.map ['', &, ";"] diff --git a/test/assignment.civet b/test/assignment.civet index 92a168aa..9fc2c3c4 100644 --- a/test/assignment.civet +++ b/test/assignment.civet @@ -371,6 +371,20 @@ describe "assignment", -> } """ + testCase """ + named destructuring declaration + --- + do {a, b^: [c, d]} := y + do {a, b^: [c, d]} .= y + do x^{a, b^: [c, d]} := y + do x^{a, b^: [c, d]} .= y + --- + { const {a, b} = y, [c, d] = b } + { let {a, b} = y, [c, d] = b } + { const x = y, {a, b} = x, [c, d] = b } + { let x = y, {a, b} = x, [c, d] = b } + """ + describe.skip "TOMAYBE", -> testCase """ multiple single line const assignments From 2c7f6642b13bb916ada9680a6df159f1b3fc49fd Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Fri, 3 Jan 2025 12:10:13 -0500 Subject: [PATCH 09/17] Named destructuring in `const`/`let` declarations --- source/parser/declaration.civet | 15 +++++++++++---- test/assignment.civet | 16 +++++++++++++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/source/parser/declaration.civet b/source/parser/declaration.civet index 55b9ffb1..3bafecee 100644 --- a/source/parser/declaration.civet +++ b/source/parser/declaration.civet @@ -79,9 +79,6 @@ function processAssignmentDeclaration(decl: ASTLeaf, pattern: Binding["pattern"] } [splices, assignments] .= gatherBindingCode pattern - subbindings := gatherSubbindings pattern - simplifyBindingProperties pattern - simplifyBindingProperties subbindings splices = splices.map (s) => [", ", s] thisAssignments := assignments.map (a) => ["", a, ";"] as const @@ -101,7 +98,7 @@ function processAssignmentDeclaration(decl: ASTLeaf, pattern: Binding["pattern"] children: [pattern, typeSuffix, initializer] } - children := [decl, binding, subbindings] + children := [decl, binding] makeNode { type: "Declaration" @@ -117,6 +114,16 @@ function processDeclarations(statements: StatementTuple[]): void for each declaration of gatherRecursiveAll statements, .type is "Declaration" { bindings } := declaration continue unless bindings? + + for i of [bindings#>..0] + binding := bindings[i] + subbindings := gatherSubbindings binding + if subbindings# + simplifyBindingProperties binding + simplifyBindingProperties subbindings + // Add subbindings after this binding + spliceChild declaration, binding, 1, binding, subbindings + for each binding of bindings { typeSuffix, initializer } .= binding diff --git a/test/assignment.civet b/test/assignment.civet index 9fc2c3c4..94f3ac21 100644 --- a/test/assignment.civet +++ b/test/assignment.civet @@ -372,7 +372,7 @@ describe "assignment", -> """ testCase """ - named destructuring declaration + named destructuring declaration shorthand --- do {a, b^: [c, d]} := y do {a, b^: [c, d]} .= y @@ -385,6 +385,20 @@ describe "assignment", -> { let x = y, {a, b} = x, [c, d] = b } """ + testCase """ + named destructuring declaration + --- + do const {a, b^: [c, d]} = y + do let {a, b^: [c, d]} = y + do const x^{a, b^: [c, d]} = y + do let x^{a, b^: [c, d]} = y + --- + { const {a, b} = y, [c, d] = b } + { let {a, b} = y, [c, d] = b } + { const x = y, {a, b} = x, [c, d] = b } + { let x = y, {a, b} = x, [c, d] = b } + """ + describe.skip "TOMAYBE", -> testCase """ multiple single line const assignments From 48b29912af8ab01a4807485bef0dcbb041f6cc59 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Fri, 3 Jan 2025 12:23:32 -0500 Subject: [PATCH 10/17] Fix duplicate bindings with named binding patterns --- source/parser/pattern-matching.civet | 5 +++-- test/switch.civet | 12 ++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/source/parser/pattern-matching.civet b/source/parser/pattern-matching.civet index ccd792ea..dbe6a051 100644 --- a/source/parser/pattern-matching.civet +++ b/source/parser/pattern-matching.civet @@ -310,7 +310,7 @@ function getPatternBlockPrefix( splices = splices.map (s) => [", ", nonMatcherBindings(s)] thisAssignments = thisAssignments.map ['', &, ";"] - duplicateDeclarations := aggregateDuplicateBindings([patternBindings, splices]) + duplicateDeclarations := aggregateDuplicateBindings([patternBindings, splices, subbindings]) [ ['', { @@ -436,8 +436,9 @@ function aggregateDuplicateBindings(bindings) for each p of props { name, value } := p - // We do not bind a property that gets matched against + // JavaScript does not bind a property that gets matched against // an array or object pattern + // (At this point, literals and other nonbinding values have been removed.) if value?.type is like "ArrayBindingPattern", "ObjectBindingPattern" continue diff --git a/test/switch.civet b/test/switch.civet index 473db550..6d84456d 100644 --- a/test/switch.civet +++ b/test/switch.civet @@ -1697,6 +1697,18 @@ describe "switch", -> console.log(this.x)} """ + testCase """ + named object with duplicate bindings + --- + switch x + [{body^: [first]}, {body^: [first]}] + console.log type + --- + function len(arr: T, length: N): arr is T & { length: N } { return arr.length === length } + if(Array.isArray(x) && len(x, 2) && typeof x[0] === 'object' && x[0] != null && 'body' in x[0] && Array.isArray(x[0].body) && len(x[0].body, 1) && typeof x[1] === 'object' && x[1] != null && 'body' in x[1] && Array.isArray(x[1].body) && len(x[1].body, 1)) {const [{body: body1}, {body: body2}] = x, [first1] = body, [first2] = body;const body = [body1, body2];const first = [first1, first2]; + console.log(type)} + """ + describe "continue switch", -> // NOTE: newline escapes prevent trim trailing whitespace editor config from messing with the test formatting testCase """ From ab72c69b873f729f1ad93b5bb6b3f84401ea7120 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Fri, 3 Jan 2025 12:35:44 -0500 Subject: [PATCH 11/17] Partial support for assignments --- source/parser/lib.civet | 14 +++++++++++--- test/assignment.civet | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/source/parser/lib.civet b/source/parser/lib.civet index 010db2a8..0c6678b8 100644 --- a/source/parser/lib.civet +++ b/source/parser/lib.civet @@ -139,6 +139,8 @@ import { adjustBindingElements gatherBindingCode gatherBindingPatternTypeSuffix + gatherSubbindings + simplifyBindingProperties } from ./binding.civet import { getPrecedence @@ -944,10 +946,16 @@ function makeGetterMethod(name, ws, value, returnType, block?: BlockStatement, k function processBindingPatternLHS(lhs, tail): void // Expand AtBindings first before gathering splices - adjustAtBindings(lhs, true) - const [splices, thisAssignments] = gatherBindingCode(lhs) + adjustAtBindings lhs, true + [splices, thisAssignments] := gatherBindingCode lhs + subbindings := gatherSubbindings lhs + simplifyBindingProperties lhs + simplifyBindingProperties subbindings // TODO: This isn't quite right for compound assignments, may need to wrap with parens and use comma to return the complete value - tail.push(...splices.map((s) => [", ", s]), ...thisAssignments.map((a) => [", ", a])) + tail.push + ...splices.map (s) => [", ", s] + ...thisAssignments.map (a) => [", ", a] + ...subbindings function processAssignments(statements): void // Move assignments/updates within LHS of assignments/updates diff --git a/test/assignment.civet b/test/assignment.civet index 94f3ac21..83d0bfa2 100644 --- a/test/assignment.civet +++ b/test/assignment.civet @@ -390,13 +390,27 @@ describe "assignment", -> --- do const {a, b^: [c, d]} = y do let {a, b^: [c, d]} = y + do var {a, b^: [c, d]} = y do const x^{a, b^: [c, d]} = y do let x^{a, b^: [c, d]} = y + do var x^{a, b^: [c, d]} = y --- { const {a, b} = y, [c, d] = b } { let {a, b} = y, [c, d] = b } + { var {a, b} = y, [c, d] = b } { const x = y, {a, b} = x, [c, d] = b } { let x = y, {a, b} = x, [c, d] = b } + { var x = y, {a, b} = x, [c, d] = b } + """ + + testCase """ + named destructuring assignment + --- + {a, b^: [c, d]} = y + //x^{a, b^: [c, d]} = y + --- + ({a, b} = y), [c, d] = b + //x^{a, b^: [c, d]} = y """ describe.skip "TOMAYBE", -> From 0974d6a87e8cc8c6e17f998a9e327e23f98cd796 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Fri, 3 Jan 2025 15:59:34 -0500 Subject: [PATCH 12/17] Fix top-level `name^pattern` in assignment --- source/parser.hera | 3 ++- source/parser/lib.civet | 2 +- test/assignment.civet | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/source/parser.hera b/source/parser.hera index 5912f49f..74245459 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -1475,6 +1475,7 @@ LeftHandSideExpression children: $0, expression, } + NamedBindingPattern CallExpression # NOTE: OptionalExpression is merged into CallExpression @@ -6586,7 +6587,7 @@ By return { $loc, token: $1 } Caret - "^" -> + "^" !"^" -> return { $loc, token: $1 } Case diff --git a/source/parser/lib.civet b/source/parser/lib.civet index 0c6678b8..d6b90c55 100644 --- a/source/parser/lib.civet +++ b/source/parser/lib.civet @@ -1102,7 +1102,7 @@ function processAssignments(statements): void message: "Slice range cannot be decreasing in assignment" break - else if lhs.type is like "ObjectBindingPattern", "ArrayBindingPattern" + else if lhs.type is like "ObjectBindingPattern", "ArrayBindingPattern", "NamedBindingPattern" processBindingPatternLHS(lhs, tail) // Extract temp refs that need to be declared from lhs gatherRecursiveAll(lhs, .type is "Ref").forEach refsToDeclare@add diff --git a/test/assignment.civet b/test/assignment.civet index 83d0bfa2..b683d18b 100644 --- a/test/assignment.civet +++ b/test/assignment.civet @@ -407,10 +407,10 @@ describe "assignment", -> named destructuring assignment --- {a, b^: [c, d]} = y - //x^{a, b^: [c, d]} = y + x^{a, b^: [c, d]} = y --- ({a, b} = y), [c, d] = b - //x^{a, b^: [c, d]} = y + x = y, {a, b} = x, [c, d] = b """ describe.skip "TOMAYBE", -> From bd01d62fec001c8bfa7b068f0b3547dd492b0406 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Fri, 3 Jan 2025 16:55:00 -0500 Subject: [PATCH 13/17] Fix @prop, splices, named patterns in `for` loops --- source/parser.hera | 2 +- source/parser/declaration.civet | 15 +++++++++++ source/parser/pattern-matching.civet | 29 ++++++++++++++++------ test/for.civet | 37 ++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 9 deletions(-) diff --git a/source/parser.hera b/source/parser.hera index 74245459..01d54e5b 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -5031,7 +5031,7 @@ ForDeclaration return { type: "ForDeclaration", children: [c, binding], - decl: c.token, + decl: c.token.trimEnd(), binding, names: binding.names, } diff --git a/source/parser/declaration.civet b/source/parser/declaration.civet index 3bafecee..3536d3bb 100644 --- a/source/parser/declaration.civet +++ b/source/parser/declaration.civet @@ -37,6 +37,7 @@ import { } from ./pattern-matching.civet import { + append convertOptionalType insertTrimmingSpace isExit @@ -159,6 +160,20 @@ function processDeclarations(statements: StatementTuple[]): void if initializer prependStatementExpressionBlock initializer, declaration + for each statement of gatherRecursiveAll statements, .type is "ForStatement" + { declaration } := statement + continue unless declaration?.type is "ForDeclaration" + { binding } := declaration + blockPrefix := getPatternBlockPrefix + binding.pattern + undefined + append declaration.decl, " " + binding.typeSuffix + simplifyBindingProperties binding + if blockPrefix? + statement.block.expressions.unshift ...blockPrefix + braceBlock statement.block + function prependStatementExpressionBlock(initializer: Initializer, statement: ASTNodeParent): ASTRef? {expression: exp} .= initializer diff --git a/source/parser/pattern-matching.civet b/source/parser/pattern-matching.civet index dbe6a051..85ea74af 100644 --- a/source/parser/pattern-matching.civet +++ b/source/parser/pattern-matching.civet @@ -46,6 +46,7 @@ import { import { gatherSubbindings gatherBindingCode + simplifyBindingProperties } from ./binding.civet import { @@ -288,7 +289,7 @@ function getPatternConditions( function getPatternBlockPrefix( pattern: PatternExpression - ref: ASTNode + ref: ASTNode // specify undefined to avoid top-level destructuring (in `for`) decl: ASTNode = "const " typeSuffix?: ASTNode ): StatementTuple[]? @@ -306,22 +307,34 @@ function getPatternBlockPrefix( [splices, thisAssignments] .= gatherBindingCode(pattern) patternBindings := nonMatcherBindings(pattern) subbindings := gatherSubbindings patternBindings + simplifyBindingProperties patternBindings + simplifyBindingProperties subbindings - splices = splices.map (s) => [", ", nonMatcherBindings(s)] + splices = splices.flatMap (s) => [", ", nonMatcherBindings(s)] thisAssignments = thisAssignments.map ['', &, ";"] duplicateDeclarations := aggregateDuplicateBindings([patternBindings, splices, subbindings]) - [ - ['', { + blockPrefix: StatementTuple[] := [] + if ref or subbindings# or splices# + children: ASTNode[] := [decl] + if ref + children.push patternBindings, typeSuffix, " = ", ref + children.push ...subbindings + children.push ...splices + unless ref + children.splice 1, 1 // remove leading comma + blockPrefix.push ['', { type: "Declaration" - children: [decl, patternBindings, typeSuffix, " = ", ref, ...subbindings, ...splices] + children + decl names: [] bindings: [] // avoid implicit return of any bindings }, ";"] - ...thisAssignments - ...duplicateDeclarations.map ['', &, ";"] - ] + blockPrefix.push ...thisAssignments + blockPrefix.push ...duplicateDeclarations.map ['', &, ";"] + return unless blockPrefix# + blockPrefix function elideMatchersFromArrayBindings(elements: ArrayBindingPatternContent): ArrayBindingPatternContent for each element of elements diff --git a/test/for.civet b/test/for.civet index ba84cd55..3b870a60 100644 --- a/test/for.civet +++ b/test/for.civet @@ -381,6 +381,43 @@ describe "for", -> } """ + testCase """ + this + --- + for @a of x + console.log @a + for [@a] of x + console.log @a + --- + for (const a of x) {this.a = a; + console.log(this.a) + } + for (const [a1] of x) {this.a = a1; + console.log(this.a) + } + """ + + testCase """ + splice + --- + for [first, ...middle, last] of x + console.log first, middle, last + --- + for (const [first, ...middle] of x) {const [last] = middle.splice(-1); + console.log(first, middle, last) + } + """ + + testCase """ + named properties + --- + for props^{array^: [lead^{item}, ...], x^, name^: {first, last}} of y + for let {array^: [lead^{item}, ...], x^, name^: {first, last}} of y + --- + for (const props of y) {const {array, x, name} = props, [lead, ...ref] = array, {item} = lead, {first, last} = name;;} + for (let {array, x, name} of y) {let [lead, ...ref1] = array, {item} = lead, {first, last} = name;;} + """ + testCase """ in optional parens --- From 693fd9c76ce0a8937bd4d644b1bd670d8303aee7 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Sat, 4 Jan 2025 13:12:54 -0500 Subject: [PATCH 14/17] Fix `for..in` support, including second declaration --- source/parser/for.civet | 61 ++++++++++++++++++++++++++++++++--------- source/parser/ref.civet | 8 ++---- test/for.civet | 8 ++++++ 3 files changed, 58 insertions(+), 19 deletions(-) diff --git a/source/parser/for.civet b/source/parser/for.civet index 3797ecb8..ac01d99d 100644 --- a/source/parser/for.civet +++ b/source/parser/for.civet @@ -12,10 +12,10 @@ import type { } from ./types.civet import { - assert checkValidLHS literalValue makeLeftHandSideExpression + makeNode makeNumericLiteral prepend startsWith @@ -292,7 +292,7 @@ function processForInOf($0: [ let assignmentNames = [...declaration.names] if declaration2 - const [, , ws2, decl2] = declaration2 // strip __ Comma __ + [, , ws2, decl2] := declaration2 // strip __ Comma __ blockPrefix.push(["", [ trimFirstSpace(ws2), decl2, " = ", counterRef ], ";"]) @@ -314,6 +314,7 @@ function processForInOf($0: [ declaration = type: "Declaration" children: ["let ", ...expRefDec, counterRef, " = 0, ", lenRef, " = ", trimFirstSpace(expRef), ".length"] + decl: "let" names: [] condition := [counterRef, " < ", lenRef, "; "] @@ -340,6 +341,10 @@ function processForInOf($0: [ // for dereferencing to get the associated value. { binding } := declaration pattern .= binding?.pattern + // Unwrap NamedBindingPattern to correctly detect "Identifier" and + // avoid double subbinding + if pattern?.type is "NamedBindingPattern" + pattern = pattern.binding if binding?.typeSuffix or ( inOf.token is "in" and declaration2 and pattern.type is not "Identifier" ) @@ -378,22 +383,25 @@ function processForInOf($0: [ hoistDec = { type: "Declaration" children: ["let ", counterRef, " = 0"] + decl: "let" names: [] } blockPrefix.push ["", { type: "Declaration" children: [trimFirstSpace(ws2), decl2, " = ", counterRef, "++"] names: decl2.names + decl: decl2.decl }, ";"] when "in" // for key, value in object // First, wrap object in ref if complex expression - expRef := maybeRef(exp) + expRef := maybeRef exp unless expRef is exp hoistDec = type: "Declaration" children: ["let ", expRef] names: [] + decl: "let" exp = type: "AssignmentExpression" children: [" ", expRef, " =", exp] @@ -402,16 +410,43 @@ function processForInOf($0: [ hasPropRef := getHelperRef("hasProp") blockPrefix.push ["", ["if (!", hasPropRef, "(", trimFirstSpace(expRef), ", ", trimFirstSpace(pattern), ")) continue"], ";"] if decl2 - blockPrefix.push ["", { - type: "Declaration" - children: [ - trimFirstSpace(ws2), decl2, " = " - trimFirstSpace(expRef) - "[", trimFirstSpace(pattern), "]" - ] - decl: decl2 - names: decl2.names - }, ";"] + trimmedPattern := trimFirstSpace pattern + expression := makeNode + type: "MemberExpression" + children: + . trimFirstSpace expRef + . makeNode + type: "Index" + expression: trimmedPattern + children: [ "[", trimmedPattern, "]" ] + blockPrefix.push ["", + if decl2.type is "ForDeclaration" + { binding, children } := decl2 + binding.children.push binding.initializer = makeNode {} + type: "Initializer" + expression + children: [" = ", expression] + makeNode + type: "Declaration" + children: [ + trimFirstSpace(ws2) + ...children + ] + bindings: [decl2.binding] + decl: decl2.decl + names: decl2.names + else + makeNode {} + type: "AssignmentExpression" + children: [ + trimFirstSpace(ws2), decl2, " = " + trimFirstSpace(expRef) + "[", trimFirstSpace(pattern), "]" + ] + names: decl2.names + lhs: decl2 + assigned: decl2 + ";"] else throw new Error `for item, index must use 'of' or 'in' instead of '${inOf.token}'` diff --git a/source/parser/ref.civet b/source/parser/ref.civet index a06389c6..85c2e861 100644 --- a/source/parser/ref.civet +++ b/source/parser/ref.civet @@ -31,13 +31,9 @@ function needsRef(expression: ASTNode, base = "ref"): ASTRef | undefined return else return makeRef base - switch (expression.type) { - case "Ref": - case "Identifier": - case "Literal": - case "Placeholder": + switch expression.type + when "Ref", "Identifier", "Literal", "Placeholder" return - } return makeRef(base) // Transform into a ref if needed diff --git a/test/for.civet b/test/for.civet index 3b870a60..98d96143 100644 --- a/test/for.civet +++ b/test/for.civet @@ -418,6 +418,14 @@ describe "for", -> for (let {array, x, name} of y) {let [lead, ...ref1] = array, {item} = lead, {first, last} = name;;} """ + testCase """ + in named properties + --- + for key^{length}, props^{array^: [lead^{item}, ...], x^, name^: {first, last}} in y + --- + for (const key in y) {const {length} = key;const props = y[key], {array, x, name} = props, [lead, ...ref] = array, {item} = lead, {first, last} = name;;} + """ + testCase """ in optional parens --- From dce0399a847d86d9deaba010e2c481248453d400 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Sat, 4 Jan 2025 13:27:27 -0500 Subject: [PATCH 15/17] Fix regex pattern matches with binding in array --- source/parser/pattern-matching.civet | 10 +++++----- test/switch.civet | 12 ++++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/source/parser/pattern-matching.civet b/source/parser/pattern-matching.civet index 85ea74af..480ad82c 100644 --- a/source/parser/pattern-matching.civet +++ b/source/parser/pattern-matching.civet @@ -383,7 +383,7 @@ function elideMatchersFromPropertyBindings(properties: ObjectBindingPatternConte bindings := nonMatcherBindings value.pattern contents = { ...p - subbinding: if bindings.type is like "ArrayBindingPattern", "ObjectBindingPattern", "Identifier" + subbinding: if bindings?.type is like "ArrayBindingPattern", "ObjectBindingPattern", "Identifier" . bindings . " = " . name @@ -403,7 +403,7 @@ function elideMatchersFromPropertyBindings(properties: ObjectBindingPatternConte else // "BindingRestProperty" p -function nonMatcherBindings(pattern: ASTNodeObject): ASTNodeObject +function nonMatcherBindings(pattern: ASTNodeObject): ASTNodeObject? switch pattern.type when "ArrayBindingPattern", "PostRestBindingElements" elements := elideMatchersFromArrayBindings pattern.elements @@ -420,12 +420,12 @@ function nonMatcherBindings(pattern: ASTNodeObject): ASTNodeObject children: pattern.children.map & is pattern.properties ? properties : & } when "NamedBindingPattern" + bindings := nonMatcherBindings pattern.pattern makeNode { ...pattern subbinding: - . nonMatcherBindings pattern.pattern - . " = " - . pattern.binding + if bindings?.type is like "ArrayBindingPattern", "ObjectBindingPattern", "Identifier" + [ bindings, " = ", pattern.binding ] } when "ConditionFragment" pattern.binding diff --git a/test/switch.civet b/test/switch.civet index 6d84456d..ef1a05d4 100644 --- a/test/switch.civet +++ b/test/switch.civet @@ -1709,6 +1709,18 @@ describe "switch", -> console.log(type)} """ + testCase """ + named array + --- + switch x + [space^/^\s*$/, number^ /^\d+$/] + console.log space, number + --- + function len(arr: T, length: N): arr is T & { length: N } { return arr.length === length } + if(Array.isArray(x) && len(x, 2) && typeof x[0] === 'string' && /^s*$/.test(x[0]) && typeof x[1] === 'string' && /^d+$/.test(x[1])) {const [space, number] = x; + console.log(space, number)} + """ + describe "continue switch", -> // NOTE: newline escapes prevent trim trailing whitespace editor config from messing with the test formatting testCase """ From 07715115ab964aad4c80166937b40436a7504b58 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Sat, 4 Jan 2025 13:30:11 -0500 Subject: [PATCH 16/17] Documentation --- civet.dev/reference.md | 64 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/civet.dev/reference.md b/civet.dev/reference.md index ab2096c4..8d385c06 100644 --- a/civet.dev/reference.md +++ b/civet.dev/reference.md @@ -641,6 +641,28 @@ a + b = c ++count *= 2 +### Multi Destructuring + +Use `name^pattern` to assign `name` while also destructuring into `pattern`: + + +[first^{x, y}, ...rest] = points + + +Shorthand for destructuring an object property and its contents: + + +{name^: {first, last}} = person + + +::: 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 @@ -1153,6 +1175,17 @@ This is particularly useful within methods. @promise := new Promise (@resolve, @reject) => +### Parameter Multi Destructuring + +[Multi destructuring](#multi-destructuring) applies to function parameters: + + +function Component(props^{ + name^: {first, last}, + counter +}) + + ### `return.value` Instead of specifying a function's return value when it returns, @@ -1657,6 +1690,16 @@ switch x console.log type, content, first +More generally, use `name^pattern` or `name^ pattern` +([multi destructuring](#multi-destructuring)) +to bind `name` while also matching `pattern`: + + +switch x + [space^ /^\s*$/, number^ /^\d+$/, ...] + console.log space, number + + 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 @@ -2094,6 +2137,27 @@ rateLimits := { } +### For Loop Multi Destructuring + +[Multi destructuring](#multi-destructuring) applies to `for..of/in` loops: + + +for item^[key, value] of map + if value and key.startsWith "a" + process item + + + +for person^{name^: {first, last}, age} of people + console.log first, last, age, person + + + +for key, value^{x, y} in items + if x > y + process key, value + + ### Infinite Loop From 53e0bd6a50494c8bc90dc32e5fed1a92ff31664a Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Sat, 4 Jan 2025 13:36:35 -0500 Subject: [PATCH 17/17] Document bindings before condition fragment --- civet.dev/reference.md | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/civet.dev/reference.md b/civet.dev/reference.md index 8d385c06..ce835879 100644 --- a/civet.dev/reference.md +++ b/civet.dev/reference.md @@ -1631,9 +1631,7 @@ switch x console.log "leading type:", type -::: info -You can also use condition fragments as patterns. -::: +You can also use condition fragments as patterns: switch x @@ -1647,21 +1645,19 @@ switch x console.log "it's something else" +You can add a binding before a condition fragment: + -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 -::: info -Aliasing object properties works the same as destructuring. -::: +Aliasing object properties works the same as destructuring: switch e @@ -1669,9 +1665,7 @@ switch e return [type, eventKey] -::: info -Patterns can aggregate duplicate bindings. -::: +Patterns can aggregate duplicate bindings: switch x @@ -2325,10 +2319,13 @@ You can also specify multiple `catch` blocks using try foo() +catch e