From ca2577d48b57d2d277d786ed289f83fc121ea416 Mon Sep 17 00:00:00 2001 From: Peeyush Kushwaha Date: Sat, 2 Nov 2024 20:17:37 +0530 Subject: [PATCH 01/11] Basic Object Comprehension Support Parses and transpiles object comprehension syntax proposed in issue #439 Tested only in limited contexts, of the 4 tests added one is failing --- source/parser.hera | 14 +++++++ test/object-comprehensions.civet | 72 ++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 test/object-comprehensions.civet diff --git a/source/parser.hera b/source/parser.hera index 25932f79..a950f938 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -3284,6 +3284,20 @@ ObjectLiteral BracedObjectLiteral NestedImplicitObjectLiteral InlineObjectLiteral + ObjectComprehension + +ObjectComprehension + OpenBrace __ "for" _ Identifier:id _ "of" _ Expression:exp __ ObjectComprehensionProps:props __ CloseBrace -> + return ["(()=>{const _results = {};\n", + "for (const ", id, " of ", exp, ") {\n", + "Object.assign(_results, {\n", + props, "\n", + "})}; return _results})()" + ] + +ObjectComprehensionProps + (ComputedPropertyName __ ":" __ Expression __)+ -> + return $0.map(entry => entry.concat([","])) BracedObjectLiteral OpenBrace:open AllowAll ( BracedObjectLiteralContent __ CloseBrace )? RestoreAll -> diff --git a/test/object-comprehensions.civet b/test/object-comprehensions.civet new file mode 100644 index 00000000..85b783f2 --- /dev/null +++ b/test/object-comprehensions.civet @@ -0,0 +1,72 @@ +{ testCase } from ./helper.civet + +describe "object comprehensions", -> + testCase """ + 1 + --- + { + for x of [1, 2, 3] + [x]: 2 * x + [x * 2]: 4 * x + } + --- + (()=>{const _results = {}; + for (const x of [1, 2, 3]) { + Object.assign(_results, { + [x]: 2 * x + ,[x * 2]: 4 * x + , + })}; return _results})() + """ + + testCase """ + scoping + --- + { + for _results of [1] + [_results]: 2 * _results + [_results * 2]: 4 * _results + } + --- + (()=>{const _results = {}; + for (const _results of [1]) { + Object.assign(_results, { + [_results]: 2 * _results + ,[_results * 2]: 4 * _results + , + })}; return _results})() + """ + + testCase """ + spacing 1 + --- + { for x of [1] + [x]: 2 * x + [x * 2]: 4 * x + } + --- + (()=>{const _results = {}; + for (const x of [1]) { + Object.assign(_results, { + [x]: 2 * x + ,[x * 2]: 4 * x + , + })}; return _results})() + """ + + testCase """ + spacing 2: indentation + --- + { for x of [1] + [x]: 2 * x + [x * 2]: 4 * x + } + --- + (()=>{const _results = {}; + for (const x of [1]) { + Object.assign(_results, { + [x]: 2 * x + ,[x * 2]: 4 * x + , + })}; return _results})() + """ From 8da16039b61f6d12b103d29c6a7451e3cbc50e54 Mon Sep 17 00:00:00 2001 From: Peeyush Kushwaha Date: Tue, 5 Nov 2024 18:27:27 +0530 Subject: [PATCH 02/11] Rewrite to use IterationExpression IterationExpressions now allowed in PropertyDefinition and assumed to be object comprehensions Code generation now handled in same way as for loop reduction logic Revamp tests to suit the new implementation All tests passing --- source/parser.hera | 37 +++++---- source/parser/function.civet | 6 ++ test/object-comprehensions.civet | 138 ++++++++++++++++++++----------- 3 files changed, 118 insertions(+), 63 deletions(-) diff --git a/source/parser.hera b/source/parser.hera index a950f938..95602103 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -3284,20 +3284,6 @@ ObjectLiteral BracedObjectLiteral NestedImplicitObjectLiteral InlineObjectLiteral - ObjectComprehension - -ObjectComprehension - OpenBrace __ "for" _ Identifier:id _ "of" _ Expression:exp __ ObjectComprehensionProps:props __ CloseBrace -> - return ["(()=>{const _results = {};\n", - "for (const ", id, " of ", exp, ") {\n", - "Object.assign(_results, {\n", - props, "\n", - "})}; return _results})()" - ] - -ObjectComprehensionProps - (ComputedPropertyName __ ":" __ Expression __)+ -> - return $0.map(entry => entry.concat([","])) BracedObjectLiteral OpenBrace:open AllowAll ( BracedObjectLiteralContent __ CloseBrace )? RestoreAll -> @@ -3314,7 +3300,7 @@ BracedObjectLiteral BracedObjectLiteralContent # Allow unnested comma-separated properties on first line, optionally # followed by nested comma-separated properties on further lines - ( PropertyDefinition ObjectPropertyDelimiter )*:line NestedPropertyDefinitions?:nested -> + ( ObjectPropLine )*:line NestedPropertyDefinitions?:nested -> line = line.flatMap(([prop, delim]) => { // Allow each prop to be a single Property object or an array of such prop = Array.isArray(prop) ? prop : [prop] @@ -3329,7 +3315,7 @@ BracedObjectLiteralContent }) return line.concat(nested || []) # As a backup, allow for arbitrary untracked indentation - ( __ PropertyDefinition ObjectPropertyDelimiter )+ -> + ( __ ObjectPropLine )+ -> return $0.flatMap(([ws, prop, delim]) => { // Allow each prop to be a single Property object or an array of such prop = Array.isArray(prop) ? prop : [prop] @@ -3344,6 +3330,14 @@ BracedObjectLiteralContent return [...prop.slice(0, prop.length-1), last] }) + +ObjectPropLine + PropertyDefinition ObjectPropertyDelimiter + +InsertSpread + "" -> + return { $loc, token: "..." } + # NOTE: Nested implicit object literal starts with an indentation. # All properties can be spaced out and must have a colon. NestedImplicitObjectLiteral @@ -3377,7 +3371,7 @@ NestedPropertyDefinitions NestedPropertyDefinition # TODO: This may be a little weird/ambiguous with single identifier shorthand - Nested:ws ( PropertyDefinition ObjectPropertyDelimiter )+:inlineProps -> + Nested:ws ( ObjectPropLine )+:inlineProps -> return inlineProps.flatMap( ([prop, delim], i) => { // Allow each prop to be a single Property object or an array of such if (!Array.isArray(prop)) prop = [prop] @@ -3553,6 +3547,15 @@ PropertyDefinition # NOTE: basic identifiers are now part of the rule above #_?:ws IdentifierReference:id -> # return prepend(ws, id) + InsertSpread:dots IterationExpression:exp -> + exp.statement.object = true + return { + type: "SpreadProperty", + children: [dots, exp], + names: exp.names, + dots, + value: exp, + } NamedProperty # NOTE: CoverInitializedName early error doesn't seem necessary with this parser diff --git a/source/parser/function.civet b/source/parser/function.civet index 541f7571..8a9a31ac 100644 --- a/source/parser/function.civet +++ b/source/parser/function.civet @@ -640,6 +640,7 @@ function iterationDeclaration(statement: IterationStatement | ForStatement) { resultsRef, block } := statement reduction := statement.type is "ForStatement" and statement.reduction + decl: "const" | "let" .= reduction ? "let" : "const" if statement.type is "IterationStatement" or statement.type is "ForStatement" if processBreakContinueWith statement @@ -661,6 +662,8 @@ function iterationDeclaration(statement: IterationStatement | ForStatement) bindings: [] } + object := statement.type is "ForStatement" and statement.object + if reduction declaration.children.push "=" + switch reduction.subtype @@ -670,6 +673,8 @@ function iterationDeclaration(statement: IterationStatement | ForStatement) when "max" then "-Infinity" when "product" then "1" else "0" + else if object + declaration.children.push "={}" else // Assign [] directly only in const case, so TypeScript can better infer if decl is "const" @@ -684,6 +689,7 @@ function iterationDeclaration(statement: IterationStatement | ForStatement) return declaration unless block.empty assignResults block, (node) => + return [ "Object.assign(", resultsRef, ",", node, ")" ] if object return [ resultsRef, ".push(", node, ")" ] unless reduction switch reduction.subtype when "some" diff --git a/test/object-comprehensions.civet b/test/object-comprehensions.civet index 85b783f2..ccbce83d 100644 --- a/test/object-comprehensions.civet +++ b/test/object-comprehensions.civet @@ -2,71 +2,117 @@ describe "object comprehensions", -> testCase """ - 1 + basic --- - { - for x of [1, 2, 3] - [x]: 2 * x - [x * 2]: 4 * x + { + for x of [1, 2, 3] + [x]: 2 * x + [x * 2]: 4 * x } --- - (()=>{const _results = {}; - for (const x of [1, 2, 3]) { - Object.assign(_results, { - [x]: 2 * x - ,[x * 2]: 4 * x - , - })}; return _results})() + ({ + ...(()=>{const results={};for (const x of [1, 2, 3]) { + Object.assign(results,({[x]: 2 * x, + [x * 2]: 4 * x})) + }return results})() + }) """ testCase """ - scoping + with other props + --- + { + a: 'a prop' + for x of [1, 2, 3] + [x]: 2 * x + [x * 2]: 4 * x + b: 'b prop' + } + --- + ({ + a: 'a prop', + ...(()=>{const results={};for (const x of [1, 2, 3]) { + Object.assign(results,({[x]: 2 * x, + [x * 2]: 4 * x})) + }return results})(), + b: 'b prop' + }) + """ + + testCase """ + mixed trailing commas --- - { - for _results of [1] - [_results]: 2 * _results - [_results * 2]: 4 * _results + { + a1: 'a1 prop' + a2: 'a2 prop', + for x of [1, 2, 3] + [x]: 2 * x + [x * 2]: 4 * x, + b1: 'b1 prop' + b2: 'b2 prop' } --- - (()=>{const _results = {}; - for (const _results of [1]) { - Object.assign(_results, { - [_results]: 2 * _results - ,[_results * 2]: 4 * _results - , - })}; return _results})() + ({ + a1: 'a1 prop', + a2: 'a2 prop', + ...(()=>{const results={};for (const x of [1, 2, 3]) { + Object.assign(results,({[x]: 2 * x, + [x * 2]: 4 * x,})) + }return results})(), + b1: 'b1 prop', + b2: 'b2 prop' + }) """ testCase """ - spacing 1 + indentation separates comprehension props from object props + --- + { + [a]: 'a computed prop' + for x of [1, 2, 3] + [x]: 2 * x + [x * 2]: 4 * x + [b]: 'b computed prop' + } + --- + ({ + [a]: 'a computed prop', + ...(()=>{const results={};for (const x of [1, 2, 3]) { + Object.assign(results,({[x]: 2 * x, + [x * 2]: 4 * x})) + }return results})(), + [b]: 'b computed prop' + }) + """ + + testCase """ + scoping --- - { for x of [1] - [x]: 2 * x - [x * 2]: 4 * x + { + for results of [1] + [results]: 2 * x } --- - (()=>{const _results = {}; - for (const x of [1]) { - Object.assign(_results, { - [x]: 2 * x - ,[x * 2]: 4 * x - , - })}; return _results})() + ({ + ...(()=>{const results1={};for (const results of [1]) { + Object.assign(results1,({[results]: 2 * x})) + }return results1})() + }) """ testCase """ - spacing 2: indentation + loop body with additional statements --- - { for x of [1] - [x]: 2 * x - [x * 2]: 4 * x + { + for x of [1] + foo bar + [x]: 2 * x } --- - (()=>{const _results = {}; - for (const x of [1]) { - Object.assign(_results, { - [x]: 2 * x - ,[x * 2]: 4 * x - , - })}; return _results})() + ({ + ...(()=>{const results={};for (const x of [1]) { + foo(bar) + Object.assign(results,({[x]: 2 * x})) + }return results})() + }) """ From f87b488cc5913207e7580a3b3320dcad054f4254 Mon Sep 17 00:00:00 2001 From: Peeyush Kushwaha Date: Tue, 5 Nov 2024 18:35:01 +0530 Subject: [PATCH 03/11] Cleanup and Polish All tests passing --- source/parser.hera | 20 ++++++++------------ source/parser/function.civet | 2 -- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/source/parser.hera b/source/parser.hera index 95602103..add829aa 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -3300,7 +3300,7 @@ BracedObjectLiteral BracedObjectLiteralContent # Allow unnested comma-separated properties on first line, optionally # followed by nested comma-separated properties on further lines - ( ObjectPropLine )*:line NestedPropertyDefinitions?:nested -> + ( PropertyDefinition ObjectPropertyDelimiter )*:line NestedPropertyDefinitions?:nested -> line = line.flatMap(([prop, delim]) => { // Allow each prop to be a single Property object or an array of such prop = Array.isArray(prop) ? prop : [prop] @@ -3315,7 +3315,7 @@ BracedObjectLiteralContent }) return line.concat(nested || []) # As a backup, allow for arbitrary untracked indentation - ( __ ObjectPropLine )+ -> + ( __ PropertyDefinition ObjectPropertyDelimiter )+ -> return $0.flatMap(([ws, prop, delim]) => { // Allow each prop to be a single Property object or an array of such prop = Array.isArray(prop) ? prop : [prop] @@ -3330,14 +3330,6 @@ BracedObjectLiteralContent return [...prop.slice(0, prop.length-1), last] }) - -ObjectPropLine - PropertyDefinition ObjectPropertyDelimiter - -InsertSpread - "" -> - return { $loc, token: "..." } - # NOTE: Nested implicit object literal starts with an indentation. # All properties can be spaced out and must have a colon. NestedImplicitObjectLiteral @@ -3371,7 +3363,7 @@ NestedPropertyDefinitions NestedPropertyDefinition # TODO: This may be a little weird/ambiguous with single identifier shorthand - Nested:ws ( ObjectPropLine )+:inlineProps -> + Nested:ws ( PropertyDefinition ObjectPropertyDelimiter )+:inlineProps -> return inlineProps.flatMap( ([prop, delim], i) => { // Allow each prop to be a single Property object or an array of such if (!Array.isArray(prop)) prop = [prop] @@ -3547,7 +3539,7 @@ PropertyDefinition # NOTE: basic identifiers are now part of the rule above #_?:ws IdentifierReference:id -> # return prepend(ws, id) - InsertSpread:dots IterationExpression:exp -> + InsertDotDotDot:dots IterationExpression:exp -> exp.statement.object = true return { type: "SpreadProperty", @@ -6470,6 +6462,10 @@ DotDotDot "…" -> return { $loc, token: "..." } +InsertDotDotDot + "" -> + return { $loc, token: "..." } + DoubleColon "::" -> return { $loc, token: $1 } diff --git a/source/parser/function.civet b/source/parser/function.civet index 8a9a31ac..11c4e5c8 100644 --- a/source/parser/function.civet +++ b/source/parser/function.civet @@ -640,7 +640,6 @@ function iterationDeclaration(statement: IterationStatement | ForStatement) { resultsRef, block } := statement reduction := statement.type is "ForStatement" and statement.reduction - decl: "const" | "let" .= reduction ? "let" : "const" if statement.type is "IterationStatement" or statement.type is "ForStatement" if processBreakContinueWith statement @@ -663,7 +662,6 @@ function iterationDeclaration(statement: IterationStatement | ForStatement) } object := statement.type is "ForStatement" and statement.object - if reduction declaration.children.push "=" + switch reduction.subtype From 1344706ef972e2c8a6443c138ee685409d16b5c6 Mon Sep 17 00:00:00 2001 From: Peeyush Kushwaha Date: Wed, 6 Nov 2024 18:17:53 +0530 Subject: [PATCH 04/11] Support other loops + many more tests All tests passing --- source/parser/function.civet | 5 +- test/object-comprehensions.civet | 111 ++++++++++++++++++++++++++++++- 2 files changed, 112 insertions(+), 4 deletions(-) diff --git a/source/parser/function.civet b/source/parser/function.civet index 11c4e5c8..5e316cba 100644 --- a/source/parser/function.civet +++ b/source/parser/function.civet @@ -661,7 +661,6 @@ function iterationDeclaration(statement: IterationStatement | ForStatement) bindings: [] } - object := statement.type is "ForStatement" and statement.object if reduction declaration.children.push "=" + switch reduction.subtype @@ -671,7 +670,7 @@ function iterationDeclaration(statement: IterationStatement | ForStatement) when "max" then "-Infinity" when "product" then "1" else "0" - else if object + else if statement.object declaration.children.push "={}" else // Assign [] directly only in const case, so TypeScript can better infer @@ -687,7 +686,7 @@ function iterationDeclaration(statement: IterationStatement | ForStatement) return declaration unless block.empty assignResults block, (node) => - return [ "Object.assign(", resultsRef, ",", node, ")" ] if object + return [ "Object.assign(", resultsRef, ",", node, ")" ] if statement.object return [ resultsRef, ".push(", node, ")" ] unless reduction switch reduction.subtype when "some" diff --git a/test/object-comprehensions.civet b/test/object-comprehensions.civet index ccbce83d..ceb1d5f9 100644 --- a/test/object-comprehensions.civet +++ b/test/object-comprehensions.civet @@ -101,7 +101,7 @@ describe "object comprehensions", -> """ testCase """ - loop body with additional statements + loop body with additional statements: single prop --- { for x of [1] @@ -116,3 +116,112 @@ describe "object comprehensions", -> }return results})() }) """ + + testCase """ + loop body with additional statements: multi prop style 1 + --- + { + for x of [1] + foo bar + { + [x]: 2 * x + [2*x]: 4 * x + } + } + --- + ({ + ...(()=>{const results={};for (const x of [1]) { + foo(bar) + Object.assign(results,({ + [x]: 2 * x, + [2*x]: 4 * x + })) + }return results})() + }) + """ + + testCase """ + loop body with additional statements: multi prop style 2 + --- + { + for x of [1] + foo bar + [x]: 2 * x + [2*x]: 4 * x + } + --- + ({ + ...(()=>{const results={};for (const x of [1]) { + foo(bar) + Object.assign(results,({[x]: 2 * x, + [2*x]: 4 * x})) + }return results})() + }) + """ + + testCase """ + on first line + --- + {for x of [1] + [x]: 2 * x + [2*x]: 4 * x } + --- + ({...(()=>{const results={};for (const x of [1]) { + Object.assign(results,({[x]: 2 * x, + [2*x]: 4 * x})) + }return results})() }) + """ + + testCase """ + on first line with other props + --- + {a: "a prop",for x of [1] + [x]: 2 * x + [2*x]: 4 * x } + --- + ({a: "a prop",...(()=>{const results={};for (const x of [1]) { + Object.assign(results,({[x]: 2 * x, + [2*x]: 4 * x})) + }return results})() }) + """ + + testCase """ + single line + --- + o := {for x of [1] [x]: 2 * x } + --- + const o = {...(()=>{const results={};for (const x of [1]({[x]: 2 * x})) {Object.assign(results,x)}return results})() } + """ + + testCase """ + + non-comprehension loop + --- + o := {...for x of [1] [x]: 2 * x } + --- + const o = {...(()=>{const results=[];for (const x of [1]({[x]: 2 * x})) {results.push(x)}return results})() } + """ + + testCase """ + while object comprehension + --- + o := {while(predicate(x)) [x]: f(x)} + --- + const o = {...(()=>{const results={};while(predicate(x)) Object.assign(results,({[x]: f(x)}));return results})()} + """ + + testCase """ + non-comprehension while loop + --- + o := {...while(predicate(x)) [x]: f(x)} + --- + const o = {...(()=>{const results=[];while(predicate(x)) results.push(({[x]: f(x)}));return results})()} + """ + + testCase """ + do while object comprehension + --- + o := {do [x]: f(x) while(predicate(x)) } + --- + const o = {...(()=>{const results={};do { Object.assign(results,({[x]: f(x)})) } while(predicate(x))return results})() } + """ From d698288344f110d56902bb7319a24be8da5c1502 Mon Sep 17 00:00:00 2001 From: Peeyush Kushwaha Date: Wed, 6 Nov 2024 21:35:38 +0530 Subject: [PATCH 05/11] Disallow for reductions and use immutability Follows code review comments in #1563 --- source/parser.hera | 21 ++++++++++++++-- test/object-comprehensions.civet | 41 +++++++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/source/parser.hera b/source/parser.hera index add829aa..a28f969c 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -3540,10 +3540,27 @@ PropertyDefinition #_?:ws IdentifierReference:id -> # return prepend(ws, id) InsertDotDotDot:dots IterationExpression:exp -> - exp.statement.object = true + let { statement } = exp + + // immutably set exp.statement.object = true + statement = { ...statement, object: true } + exp = { + ...exp, + statement, + children: exp.children.map(($) => $ === exp.statement ? statement : $), + } + + const children = [dots, exp] + if (statement.reduction) { + children.unshift({ + type: "Error", + message: "Reduction loops are forbidden in object literals", + }) + } + return { type: "SpreadProperty", - children: [dots, exp], + children, names: exp.names, dots, value: exp, diff --git a/test/object-comprehensions.civet b/test/object-comprehensions.civet index ceb1d5f9..094eac58 100644 --- a/test/object-comprehensions.civet +++ b/test/object-comprehensions.civet @@ -1,4 +1,4 @@ -{ testCase } from ./helper.civet +{ testCase, throws } from ./helper.civet describe "object comprehensions", -> testCase """ @@ -225,3 +225,42 @@ describe "object comprehensions", -> --- const o = {...(()=>{const results={};do { Object.assign(results,({[x]: f(x)})) } while(predicate(x))return results})() } """ + + testCase """ + empty loop is allowed + --- + { + for x of [{a: b}, {c:d}] + } + --- + ({ + ...(()=>{const results={};for (const x of [{a: b}, {c:d}]) {Object.assign(results,x)}return results})() + }) + """ + + testCase """ + simple expressionization is allowed + --- + { + for x of [1, 2, 3] + f(x) + } + --- + ({ + ...(()=>{const results={};for (const x of [1, 2, 3]) { + Object.assign(results,f(x)) + }return results})() + }) + """ + + throws """ + for reductions are disallowed + --- + { + a: b, + for count x of [1, 2, 3] + x % 2 == 0 + } + --- + ParseErrors: unknown:3:3 Reduction loops are forbidden in object literals + """ From 7ac095e35f834423c7178904a096b7d74609a560 Mon Sep 17 00:00:00 2001 From: Peeyush Kushwaha Date: Wed, 6 Nov 2024 22:56:57 +0530 Subject: [PATCH 06/11] Add some reference documentation --- civet.dev/reference.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/civet.dev/reference.md b/civet.dev/reference.md index af8db6f5..7c961279 100644 --- a/civet.dev/reference.md +++ b/civet.dev/reference.md @@ -1937,6 +1937,31 @@ min := for min item of array max := for max item of array +### Object Comprehensions + +Loops can also accumulate their body values into an object. +When any loop is found is found within an object expression, +its body value is spread into the containing object. + + +object := {a: 1, b: 2, c: 3} +doubled := { + for key in object + [key]: 2 * object[key] +} + + +The loop can exist anywhere a property is expected. +It can be freely mixed with other object properties. + + +rateLimits := { + admin: Infinity, + for user of users: + [user.name]: getRemainingLimit(user) +} + + ### Infinite Loop From 548bc91f1ca021ed1ed6fa64936a11ff6b211b7a Mon Sep 17 00:00:00 2001 From: Peeyush Kushwaha Date: Wed, 6 Nov 2024 23:11:27 +0530 Subject: [PATCH 07/11] Relax whitespace In line with other PropertyDefinition rules --- source/parser.hera | 5 +++-- test/object-comprehensions.civet | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/source/parser.hera b/source/parser.hera index a28f969c..a55809da 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -3539,7 +3539,7 @@ PropertyDefinition # NOTE: basic identifiers are now part of the rule above #_?:ws IdentifierReference:id -> # return prepend(ws, id) - InsertDotDotDot:dots IterationExpression:exp -> + _?:ws InsertDotDotDot:dots IterationExpression:exp -> let { statement } = exp // immutably set exp.statement.object = true @@ -3550,7 +3550,7 @@ PropertyDefinition children: exp.children.map(($) => $ === exp.statement ? statement : $), } - const children = [dots, exp] + const children = [ws, dots, exp] if (statement.reduction) { children.unshift({ type: "Error", @@ -3566,6 +3566,7 @@ PropertyDefinition value: exp, } + NamedProperty # NOTE: CoverInitializedName early error doesn't seem necessary with this parser # NOTE: Using PostfixedExpression to allow If/Switch expressions and postfixes diff --git a/test/object-comprehensions.civet b/test/object-comprehensions.civet index 094eac58..6c4dcbb3 100644 --- a/test/object-comprehensions.civet +++ b/test/object-comprehensions.civet @@ -175,11 +175,11 @@ describe "object comprehensions", -> testCase """ on first line with other props --- - {a: "a prop",for x of [1] + { a: "a prop", for x of [1] [x]: 2 * x [2*x]: 4 * x } --- - ({a: "a prop",...(()=>{const results={};for (const x of [1]) { + ({ a: "a prop", ...(()=>{const results={};for (const x of [1]) { Object.assign(results,({[x]: 2 * x, [2*x]: 4 * x})) }return results})() }) @@ -205,9 +205,9 @@ describe "object comprehensions", -> testCase """ while object comprehension --- - o := {while(predicate(x)) [x]: f(x)} + o := { while(predicate(x)) [x]: f(x)} --- - const o = {...(()=>{const results={};while(predicate(x)) Object.assign(results,({[x]: f(x)}));return results})()} + const o = { ...(()=>{const results={};while(predicate(x)) Object.assign(results,({[x]: f(x)}));return results})()} """ testCase """ From ef813cf36afeaaa3105afec50d0e995c226c5721 Mon Sep 17 00:00:00 2001 From: Peeyush Kushwaha Date: Thu, 7 Nov 2024 20:31:26 +0530 Subject: [PATCH 08/11] Prefer do...while and "loop" braceless loops Prefer them over "do" and "loop" prop keys --- source/parser.hera | 54 +++++++++++++++++--------------- test/object-comprehensions.civet | 41 +++++++++++++++++++++++- test/object.civet | 4 +-- 3 files changed, 70 insertions(+), 29 deletions(-) diff --git a/source/parser.hera b/source/parser.hera index a55809da..42d2d53b 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -3422,6 +3422,34 @@ ObjectPropertyDelimiter # https://262.ecma-international.org/#prod-PropertyDefinition # NOTE: Must start on same line PropertyDefinition + # Safe to prioritize this over others as it's unlikely that a loop is matched unless the user intended to put the loop there. This is also required for giving braceless-do loop priority over interpreting do as a PropertyName + _?:ws InsertDotDotDot:dots IterationExpression:exp -> + debugger + let { statement } = exp + + // immutably set exp.statement.object = true + statement = { ...statement, object: true } + exp = { + ...exp, + statement, + children: exp.children.map(($) => $ === exp.statement ? statement : $), + } + + const children = [ws, dots, exp] + if (statement.reduction) { + children.unshift({ + type: "Error", + message: "Reduction loops are forbidden in object literals", + }) + } + + return { + type: "SpreadProperty", + children, + names: exp.names, + dots, + value: exp, + } _?:ws NamedProperty:prop -> return prepend(ws, prop) @@ -3539,32 +3567,6 @@ PropertyDefinition # NOTE: basic identifiers are now part of the rule above #_?:ws IdentifierReference:id -> # return prepend(ws, id) - _?:ws InsertDotDotDot:dots IterationExpression:exp -> - let { statement } = exp - - // immutably set exp.statement.object = true - statement = { ...statement, object: true } - exp = { - ...exp, - statement, - children: exp.children.map(($) => $ === exp.statement ? statement : $), - } - - const children = [ws, dots, exp] - if (statement.reduction) { - children.unshift({ - type: "Error", - message: "Reduction loops are forbidden in object literals", - }) - } - - return { - type: "SpreadProperty", - children, - names: exp.names, - dots, - value: exp, - } NamedProperty diff --git a/test/object-comprehensions.civet b/test/object-comprehensions.civet index 6c4dcbb3..fc355254 100644 --- a/test/object-comprehensions.civet +++ b/test/object-comprehensions.civet @@ -219,13 +219,52 @@ describe "object comprehensions", -> """ testCase """ - do while object comprehension + do...while object comprehension --- o := {do [x]: f(x) while(predicate(x)) } --- const o = {...(()=>{const results={};do { Object.assign(results,({[x]: f(x)})) } while(predicate(x))return results})() } """ + testCase """ + do...while multi-line + --- + i .= 1 + squares := { + do { + [i]: i * i + } while i++ < 10 + } + --- + let i = 1 + const squares = { + ...(()=>{const results={};do { + Object.assign(results,({[i]: i * i})) + } while (i++ < 10)return results})() + } + """ + + testCase """ + "loop" + --- + o := { + a: "a prop", + loop + break if predicate(i) + i++ + [i]: f(i) + } + --- + const o = { + a: "a prop", + ...(()=>{const results={};while(true) { + if (predicate(i)) { break } + i++ + Object.assign(results,({[i]: f(i)})) + }return results})() + } + """ + testCase """ empty loop is allowed --- diff --git a/test/object.civet b/test/object.civet index 88cb2316..baeb5da2 100644 --- a/test/object.civet +++ b/test/object.civet @@ -1408,9 +1408,9 @@ describe "object", -> testCase """ with reserved word keys --- - state.{and,await,break,case,catch,class,const,continue,debugger,default,delete,do,else,enum,export,extends} + state.{and,await,break,case,catch,class,const,continue,debugger,default,delete,else,enum,export,extends} --- - ({and:state.and,await:state.await,break:state.break,case:state.case,catch:state.catch,class:state.class,const:state.const,continue:state.continue,debugger:state.debugger,default:state.default,delete:state.delete,do:state.do,else:state.else,enum:state.enum,export:state.export,extends:state.extends}) + ({and:state.and,await:state.await,break:state.break,case:state.case,catch:state.catch,class:state.class,const:state.const,continue:state.continue,debugger:state.debugger,default:state.default,delete:state.delete,else:state.else,enum:state.enum,export:state.export,extends:state.extends}) """ testCase """ From e833a67665c136917f60a8f2016195de26833a59 Mon Sep 17 00:00:00 2001 From: Peeyush Kushwaha Date: Thu, 7 Nov 2024 20:41:00 +0530 Subject: [PATCH 09/11] Update object comprehensions docs - Add example with a do...while loop - Fix colon in an existing example --- civet.dev/reference.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/civet.dev/reference.md b/civet.dev/reference.md index 7c961279..43517cb9 100644 --- a/civet.dev/reference.md +++ b/civet.dev/reference.md @@ -1940,7 +1940,7 @@ max := for max item of array ### Object Comprehensions Loops can also accumulate their body values into an object. -When any loop is found is found within an object expression, +When any loop is found is found within a braced object expression, its body value is spread into the containing object. @@ -1951,13 +1951,22 @@ doubled := { } + +i .= 1 +squares := { + do + [i]: i * i + while i++ < 10 +} + + The loop can exist anywhere a property is expected. It can be freely mixed with other object properties. rateLimits := { admin: Infinity, - for user of users: + for user of users [user.name]: getRemainingLimit(user) } From 24283e53726820f9bbd39acc755bfccd5fa36a44 Mon Sep 17 00:00:00 2001 From: Peeyush Kushwaha Date: Thu, 7 Nov 2024 22:24:50 +0530 Subject: [PATCH 10/11] Remove debugger statement --- source/parser.hera | 1 - 1 file changed, 1 deletion(-) diff --git a/source/parser.hera b/source/parser.hera index 42d2d53b..e93f29f4 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -3424,7 +3424,6 @@ ObjectPropertyDelimiter PropertyDefinition # Safe to prioritize this over others as it's unlikely that a loop is matched unless the user intended to put the loop there. This is also required for giving braceless-do loop priority over interpreting do as a PropertyName _?:ws InsertDotDotDot:dots IterationExpression:exp -> - debugger let { statement } = exp // immutably set exp.statement.object = true From a3fc074cee5fde7545e5962e41e300e1e6ed7436 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Thu, 7 Nov 2024 13:42:21 -0500 Subject: [PATCH 11/11] Prioritize `{do}` as object property, not iteration --- source/parser.hera | 9 ++++++++- test/object.civet | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/source/parser.hera b/source/parser.hera index e93f29f4..6d5b05ea 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -3422,9 +3422,16 @@ ObjectPropertyDelimiter # https://262.ecma-international.org/#prod-PropertyDefinition # NOTE: Must start on same line PropertyDefinition - # Safe to prioritize this over others as it's unlikely that a loop is matched unless the user intended to put the loop there. This is also required for giving braceless-do loop priority over interpreting do as a PropertyName + # NOTE: This needs to be above NamedProperty so that + # a `do` block doesn't get treated as {do} property _?:ws InsertDotDotDot:dots IterationExpression:exp -> let { statement } = exp + // Treat empty-bodied `do` and `loop` (which require no expression) + // as keys instead of iterations + if (exp.block.implicit && + (statement.type === "DoStatement" || statement.subtype === "loop")) { + return $skip + } // immutably set exp.statement.object = true statement = { ...statement, object: true } diff --git a/test/object.civet b/test/object.civet index baeb5da2..88cb2316 100644 --- a/test/object.civet +++ b/test/object.civet @@ -1408,9 +1408,9 @@ describe "object", -> testCase """ with reserved word keys --- - state.{and,await,break,case,catch,class,const,continue,debugger,default,delete,else,enum,export,extends} + state.{and,await,break,case,catch,class,const,continue,debugger,default,delete,do,else,enum,export,extends} --- - ({and:state.and,await:state.await,break:state.break,case:state.case,catch:state.catch,class:state.class,const:state.const,continue:state.continue,debugger:state.debugger,default:state.default,delete:state.delete,else:state.else,enum:state.enum,export:state.export,extends:state.extends}) + ({and:state.and,await:state.await,break:state.break,case:state.case,catch:state.catch,class:state.class,const:state.const,continue:state.continue,debugger:state.debugger,default:state.default,delete:state.delete,do:state.do,else:state.else,enum:state.enum,export:state.export,extends:state.extends}) """ testCase """