Skip to content

Commit

Permalink
Merge pull request #1656 from DanielXMoore/spread-for
Browse files Browse the repository at this point in the history
Multiple items and spreads in array comprehensions
  • Loading branch information
edemaine authored Dec 24, 2024
2 parents f065bba + 91bfe58 commit fd1aaa4
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 16 deletions.
20 changes: 19 additions & 1 deletion civet.dev/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1818,7 +1818,7 @@ for key: keyof typeof object, value in object
### Loop Expressions
If needed, loops automatically assemble an Array of the last value
If needed, loops automatically assemble an array of the last value
within the body of the loop for each completed iteration.
<Playground>
Expand Down Expand Up @@ -1850,6 +1850,24 @@ so you cannot use `return` inside such a loop,
nor can you `break` or `continue` any outer loop.
:::
You can also accumulate multiple items and/or spreads:
<Playground>
function flatJoin<T>(list: T[][], sep: T): T[]
for sublist, i of list
if i
sep, ...sublist
else
...sublist
</Playground>
<Playground>
flatImage :=
for x of [0...nx]
...for y of [0...ny]
image.get x, y
</Playground>
If you don't specify a body, `for` loops list the item being iterated over:
<Playground>
Expand Down
80 changes: 71 additions & 9 deletions source/parser.hera
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,56 @@ CommaExpression
if($2.length == 0) return $1
return $0

# Variation on CommaExpression that allows ... spreads,
# which we allow in statements for the sake of expressionized iterations
CommaExpressionSpread
_?:ws DotDotDot _?:ws2 IterationActualStatement:iteration ->
if (iteration.subtype === "do" || iteration.subtype === "comptime") return $skip
if (ws2) {
if (ws) {
ws = [ws, ws2]
} else {
ws = ws2
}
}
iteration = { ...iteration, resultsParent: true }
return prepend(ws, iteration)
AssignmentExpressionSpread ( CommaDelimiter AssignmentExpressionSpread )* ->
if($2.length == 0) return $1
return $0

AssignmentExpressionSpread
_? DotDotDot AssignmentExpression:expression ->
return {
type: "SpreadElement",
children: $0,
expression,
names: expression.names,
}
AssignmentExpression:expression ( _? DotDotDot )? ->
if (!$2) return $1
return {
type: "SpreadElement",
children: [ ...$2, $1 ],
expression,
names: expression.names,
}
_? DotDotDot AssignmentExpression:expression ->
return {
type: "SpreadElement",
children: $0,
expression,
names: expression.names,
}
AssignmentExpression:expression ( _? DotDotDot )? ->
if (!$2) return $1
return {
type: "SpreadElement",
children: [ ...$2, $1 ],
expression,
names: expression.names,
}

# https://262.ecma-international.org/#prod-Arguments
Arguments
ExplicitArguments
Expand Down Expand Up @@ -2649,7 +2699,14 @@ BareBlock
EmptyBareBlock

ThenClause
Then SingleLineStatements -> $2
Then ThenBlock -> $2

# `then` can be followed by nested block (unlike CoffeeScript)
# or same-line statements separated by `;`, or empty
ThenBlock
NoBlock EmptyBlock -> $2
ImplicitNestedBlock
SingleLineStatements

BracedThenClause
&Then InsertOpenBrace:open ThenClause:exp InsertCloseBrace:close ->
Expand Down Expand Up @@ -4362,12 +4419,7 @@ Statement
KeywordStatement
VariableStatement
IfStatement !ShouldExpressionize -> $1
IterationStatement !ShouldExpressionize ->
// Leave generator forms for IterationExpression
if ($1.generator) return $skip
// Leave `for` reductions for IterationExpression
if ($1.reduction) return $skip
return $1
IterationActualStatement
SwitchStatement !ShouldExpressionize -> $1
TryStatement !ShouldExpressionize -> $1
FinallyClause
Expand All @@ -4383,6 +4435,14 @@ Statement

# NOTE: no WithStatement

IterationActualStatement
IterationStatement !ShouldExpressionize ->
// Leave generator forms for IterationExpression
if ($1.generator) return $skip
// Leave `for` reductions for IterationExpression
if ($1.reduction) return $skip
return $1

# NOTE: Leave statement with trailing call expressions or pipe operator
# to be expressionized by
# ExpressionizedStatementWithTrailingCallExpressions
Expand Down Expand Up @@ -4473,7 +4533,7 @@ LabelledItem
# https://262.ecma-international.org/#prod-IfStatement
IfStatement
# NOTE: Allow indentation in condition when there's an explicit `then` clause
( If / Unless ):kind _?:ws BoundedCondition:condition Then BlockOrEmpty:block ElseClause?:e ->
( If / Unless ):kind _?:ws BoundedCondition:condition ThenClause:block ElseClause?:e ->
if (kind.negated) {
kind = { ...kind, token: "if" }
condition = negateCondition(condition)
Expand Down Expand Up @@ -5397,7 +5457,9 @@ CommaExpressionStatement
# NOTE: semi-colons are being handled elsewhere
# NOTE: Shouldn't need negative lookahead if shadowed in the proper order
# NOTE: CommaExpression allows , operator
CommaExpression ->
# NOTE: CommaExpressionSpread allows ... spreads,
# which are useful within expressionized iterations
CommaExpressionSpread ->
// Wrap object literal with parens to disambiguate from block statements.
// Also wrap nameless functions from `->` expressions with parens
// as needed in JS.
Expand Down
2 changes: 1 addition & 1 deletion source/parser/declaration.civet
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ function prependStatementExpressionBlock(initializer: Initializer, statement: AS
ws = exp[0]
exp = exp[1]!

return unless exp?.type is "StatementExpression"
return unless exp is like {type: "StatementExpression"}, {type: "SpreadElement", expression: {type: "StatementExpression"}}

pre: ASTNode[] := []
statementExp := exp.statement
Expand Down
20 changes: 19 additions & 1 deletion source/parser/function.civet
Original file line number Diff line number Diff line change
Expand Up @@ -677,7 +677,24 @@ function wrapIterationReturningResults(
// to implement `implicitlyReturned`
return if statement.resultsRef?

resultsRef := statement.resultsRef = makeRef "results"
// Inherit parent's resultsRef if requested
if statement.resultsParent
{ ancestor } := findAncestor statement,
.type is "ForStatement" or .type is "IterationStatement"
isFunction
unless ancestor
statement.children.unshift
type: "Error"
message: "Could not find ancestor of spread iteration"
return
resultsRef := statement.resultsRef = ancestor.resultsRef
iterationDefaultBody statement
{ block } := statement
unless block.empty
assignResults block, (node) => [ resultsRef, ".push(", node, ")" ]
return

resultsRef := statement.resultsRef ?= makeRef "results"

declaration := iterationDeclaration statement
{ ancestor, child } := findAncestor statement, .type is "BlockStatement"
Expand Down Expand Up @@ -1145,6 +1162,7 @@ function expressionizeIteration(exp: IterationExpression): void

statements =
. ["", statement]

else
resultsRef := statement.resultsRef ??= makeRef "results"

Expand Down
4 changes: 2 additions & 2 deletions source/parser/lib.civet
Original file line number Diff line number Diff line change
Expand Up @@ -754,7 +754,7 @@ function makeExpressionStatement(expression: ASTNode): ASTNode
if Array.isArray(expression) and expression[1]?[0]?[0]?[1]?.token is "," // CommaExpression
[
makeExpressionStatement expression[0]
expression[1].map ([comma, exp]) => // CommaDelimiter AssignmentExpression
expression[1].map [comma, exp] => // CommaDelimiter AssignmentExpression
[comma, makeExpressionStatement exp]
]
else if expression?.type is "ObjectExpression" or
Expand Down Expand Up @@ -1002,7 +1002,7 @@ function processAssignments(statements): void

let block?: BlockStatement
// Extract StatementExpression as block
if exp.parent?.type is "BlockStatement" and !$1.-1?.-1?.special// can only prepend to assignments that are children of blocks
if blockContainingStatement(exp) and !$1.-1?.-1?.special// can only prepend to assignments that are children of blocks
block = makeBlockFragment()
if ref := prependStatementExpressionBlock(
{type: "Initializer", expression: $2, children: [undefined, undefined, $2]}
Expand Down
2 changes: 2 additions & 0 deletions source/parser/types.civet
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ export type IterationStatement
generator?: ASTNode
object?: boolean
resultsRef: ASTRef?
resultsParent?: boolean // inherit resultsRef from parent loop

export type BreakStatement
type: "BreakStatement"
Expand Down Expand Up @@ -425,6 +426,7 @@ export type ForStatement
hoistDec: ASTNode
generator?: ASTNode
resultsRef: ASTRef?
resultsParent?: boolean // inherit resultsRef from parent loop
reduction?: ForReduction
object?: boolean

Expand Down
82 changes: 82 additions & 0 deletions test/for.civet
Original file line number Diff line number Diff line change
Expand Up @@ -1527,3 +1527,85 @@ describe "for", ->
---
if (!(()=>{let results=true;for (const f of facesHit) {results = false; break}return results})()) { return false }
"""

describe "spreads", ->
testCase """
comma in body
---
function concatPairs(pairs)
for [x, y] of pairs
x, y
---
function concatPairs(pairs) {
const results=[];for (const [x, y] of pairs) {
results.push(x, y)
};return results;
}
"""

testCase """
spread in body
---
function concat(arrays)
for array of arrays
...array
---
function concat(arrays) {
const results=[];for (const array of arrays) {
results.push(...array)
};return results;
}
"""

testCase """
reverse spread in body
---
function concat(arrays)
for array of arrays
array ...
---
function concat(arrays) {
const results=[];for (const array of arrays) {
results.push( ...array)
};return results;
}
"""

testCase """
multiple spreads in body
---
result :=
for item of items
item.pre, ...item.mid, item.post, ... item.tail
---
const results=[];for (const item of items) {
results.push(item.pre, ...item.mid, item.post, ... item.tail)
};const result =results
"""

testCase """
spread iteration
---
function flatSquare(arrays)
for array of arrays
... for item of array
item * item
---
function flatSquare(arrays) {
const results=[];for (const array of arrays) {
for (const item of array) {
results.push(item * item)
}
};return results;
}
"""

throws """
spread iteration without parent
---
function flatSquare(arrays)
... for item of array
item * item
---
ParseErrors: unknown:2:3 Could not find ancestor of spread iteration
"""
20 changes: 18 additions & 2 deletions test/if.civet
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,8 @@ describe "if", ->
then
console.log 'yes'
---
if((()=>{let results=true;for (const x in y) { if (!(
x)) {results = false; break} }return results})()) {
if(
(()=>{let results=true;for (const x in y) { if (!x) {results = false; break} }return results})()) {
console.log('yes')
}
"""
Expand Down Expand Up @@ -345,6 +345,14 @@ describe "if", ->
if (x) y
"""

testCase """
if then with braces
---
if x then {y}
---
if (x) ({y})
"""

testCase """
if with multiple semicolon-separated statements
---
Expand Down Expand Up @@ -955,6 +963,14 @@ describe "if", ->
x = ((y? "a" : "b"))
"""

testCase """
parenthesized expression with then and braces
---
x = (if y then {z})
---
x = ((y? ({z}):void 0))
"""

testCase """
parenthesized expression with empty then and else on one line
---
Expand Down

0 comments on commit fd1aaa4

Please sign in to comment.