diff --git a/civet.dev/reference.md b/civet.dev/reference.md index af8db6f5..43517cb9 100644 --- a/civet.dev/reference.md +++ b/civet.dev/reference.md @@ -1937,6 +1937,40 @@ 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 a braced 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] +} + + + +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 + [user.name]: getRemainingLimit(user) +} + + ### Infinite Loop diff --git a/source/parser.hera b/source/parser.hera index 25932f79..6d5b05ea 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -3422,6 +3422,40 @@ ObjectPropertyDelimiter # https://262.ecma-international.org/#prod-PropertyDefinition # NOTE: Must start on same line PropertyDefinition + # 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 } + 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) @@ -3540,6 +3574,7 @@ PropertyDefinition #_?:ws IdentifierReference:id -> # return prepend(ws, id) + NamedProperty # NOTE: CoverInitializedName early error doesn't seem necessary with this parser # NOTE: Using PostfixedExpression to allow If/Switch expressions and postfixes @@ -6453,6 +6488,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 541f7571..5e316cba 100644 --- a/source/parser/function.civet +++ b/source/parser/function.civet @@ -670,6 +670,8 @@ function iterationDeclaration(statement: IterationStatement | ForStatement) when "max" then "-Infinity" when "product" then "1" else "0" + else if statement.object + declaration.children.push "={}" else // Assign [] directly only in const case, so TypeScript can better infer if decl is "const" @@ -684,6 +686,7 @@ function iterationDeclaration(statement: IterationStatement | ForStatement) return declaration unless block.empty assignResults block, (node) => + 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 new file mode 100644 index 00000000..fc355254 --- /dev/null +++ b/test/object-comprehensions.civet @@ -0,0 +1,305 @@ +{ testCase, throws } from ./helper.civet + +describe "object comprehensions", -> + testCase """ + basic + --- + { + 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 """ + 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 + --- + { + 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' + } + --- + ({ + 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 """ + 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 results of [1] + [results]: 2 * x + } + --- + ({ + ...(()=>{const results1={};for (const results of [1]) { + Object.assign(results1,({[results]: 2 * x})) + }return results1})() + }) + """ + + testCase """ + loop body with additional statements: single prop + --- + { + for x of [1] + foo bar + [x]: 2 * x + } + --- + ({ + ...(()=>{const results={};for (const x of [1]) { + foo(bar) + Object.assign(results,({[x]: 2 * x})) + }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})() } + """ + + 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 + --- + { + 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 + """