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
+ """