Skip to content

Commit

Permalink
Merge pull request #1505 from DanielXMoore/for-by
Browse files Browse the repository at this point in the history
Range loops can provide custom skip via `by` (not just in CoffeeScript)
  • Loading branch information
edemaine authored Oct 24, 2024
2 parents d2e2cd6 + 10127d7 commit bec932c
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 100 deletions.
5 changes: 5 additions & 0 deletions civet.dev/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1835,6 +1835,11 @@ for i of [0...array.length]
array[i] = array[i].toString()
</Playground>
<Playground>
for i of [0...n] by 2
console.log i, "is even"
</Playground>
<Playground>
for [1..5]
attempt()
Expand Down
3 changes: 2 additions & 1 deletion notes/Comparison-to-CoffeeScript.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ Things Kept from CoffeeScript
- `when` inside `switch` automatically breaks
- Multiple `,` separated `case`/`when` expressions
- `else``default` in `switch`
- `for ... when <condition>`
- `for ... by <step>` when looping over range literals
- Range literals `[0...10]`, `[a..b]`, `[x - 2 .. x + 2]`
- Array slices `list[0...2]``list.slice(0, 2)`
- Slice assignment `numbers[3..6] = [-3, -4, -5, -6]``numbers.splice(3, 4, ...[-3, -4, -5, -6])`
Expand Down Expand Up @@ -67,7 +69,6 @@ Civet.
- `for from` (use JS `for of`, `"civet coffeeCompat"`, or `"civet coffeeForLoops"`)
- `for in` (use `for each of`, or `"civet coffeeCompat"`, or `"civet coffeeForLoops"`)
- `for own of` (use `for own in`, or `"civet coffeeCompat"`, or `"civet coffeeForLoops"`)
- `for ... when <condition>` (use `continue if exp` inside loop, `"civet coffeeCompat"`, or `"civet coffeeForLoops"`)
- `a ? b` (use `a ?? b`, though it doesn't check for undeclared variables; `"civet coffeeCompat"`, or `"civet coffeeBinaryExistential"` enables `a ? b` at the cost of losing JS ternary operator)
- `a of b` (use `a in b` as in JS, or `"civet coffeeCompat"`, or `"civet coffeeOf"`)
- `a not of b` (use `a not in b`, or `"civet coffeeCompat"`, or `"civet coffeeOf"`)
Expand Down
10 changes: 7 additions & 3 deletions source/generate.civet
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,17 @@ function gen(root: ASTNode, options: Options): string
offset
return ""

if node.$loc?
if "$loc" in node
{token, $loc} := node
updateSourceMap? token, $loc.pos
updateSourceMap? token, $loc.pos if $loc?
//console.log 'set', node, options.sourceMap.data.srcLine, options.sourceMap.data.srcColumn if options?.sourceMap?
return token

if !node.children
unless node.children
// Occasionally we create ASTLeaf nodes with no $loc field
if node.token?
return node.token

switch node.type
when "Ref"
throw new Error(`Unpopulated ref ${stringify node}`)
Expand Down
12 changes: 6 additions & 6 deletions source/parser.hera
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ ForbiddenImplicitCalls

# Binary operators that are reserved in that context
ReservedBinary
/(as|of|satisfies|then|when|implements|xor|xnor)(?!\p{ID_Continue}|[\u200C\u200D$])/
/(as|of|by|satisfies|then|when|implements|xor|xnor)(?!\p{ID_Continue}|[\u200C\u200D$])/

ArgumentsWithTrailingMemberExpressions
Arguments:args AllowedTrailingMemberExpressions:trailing ->
Expand Down Expand Up @@ -4550,7 +4550,7 @@ ForStatementControlWithWhen
}
return {
...control,
blockPrefix: [...control.blockPrefix,
blockPrefix: [...control.blockPrefix ?? [],
["", {
type: "IfStatement",
then: block,
Expand Down Expand Up @@ -4606,7 +4606,7 @@ CoffeeForStatementParameters
const lenRef = makeRef("len")

if (exp.type === "RangeExpression") {
return forRange(open, declaration, exp, step?.[2], close)
return forRange(open, declaration, exp, step && prepend(trimFirstSpace(step[0]), trimFirstSpace(step[2])), close)
}

// If exp isn't a simple identifier use a ref
Expand Down Expand Up @@ -4718,10 +4718,10 @@ ForStatementParameters
# NOTE: Consolidated declarations
# NOTE: Consolidated optional 'await'
( Await __ )? ( (Each / Own) __ )? ( OpenParen __ ) ForInOfDeclaration ( __ Comma __ ForInOfDeclaration )? __ ( In / Of ) ExpressionWithObjectApplicationForbidden ( __ By ExpressionWithObjectApplicationForbidden )? ( __ CloseParen ) ->
return processForInOf($0, getHelperRef)
return processForInOf($0)
# NOTE: Added optional parens
( Await __ )? ( (Each / Own) __ )? InsertOpenParen ForInOfDeclaration ( __ Comma __ ForInOfDeclaration )? __ ( In / Of ) ExpressionWithObjectApplicationForbidden ( __ By ExpressionWithObjectApplicationForbidden )? InsertCloseParen ->
return processForInOf($0, getHelperRef)
return processForInOf($0)
ForRangeParameters

ForRangeParameters
Expand Down Expand Up @@ -6673,7 +6673,7 @@ JSXImplicitFragment
],
jsxChildren: [$1].concat($2.map(([, tag]) => tag)),
}
const type = typeOfJSX(jsx, config, getHelperRef)
const type = typeOfJSX(jsx, config)
return type ? [
{ ts: true, children: ["("] },
jsx,
Expand Down
85 changes: 50 additions & 35 deletions source/parser/for.civet
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,26 @@ import type {
Children
RangeDots
RangeExpression
Whitespace
} from ./types.civet

import {
insertTrimmingSpace
literalValue
makeLeftHandSideExpression
makeNumericLiteral
prepend
trimFirstSpace
} from ./util.civet

import {
makeRef
maybeRef
} from ./ref.civet

import {
getHelperRef
} from ./helper.civet

function processRangeExpression(start: ASTNode, ws1: ASTNode, range: RangeDots, end: ASTNode)
ws1 = [ws1, range.children[0]] // whitespace before ..
ws2 := range.children[1] // whitespace after ..
Expand Down Expand Up @@ -127,17 +134,21 @@ function forRange(

let stepRef, asc: boolean?
if stepExp
stepExp = insertTrimmingSpace(stepExp, "")
stepExp = trimFirstSpace(stepExp)
stepRef = maybeRef(stepExp, "step")
else if infinite
stepExp = stepRef = "1"
stepExp = stepRef = makeNumericLiteral 1
else if increasing?
if increasing
stepExp = stepRef = "1"
stepExp = stepRef = makeNumericLiteral 1
asc = true
else
stepExp = stepRef = "-1"
stepExp = stepRef = makeNumericLiteral -1
asc = false
stepValue := if stepExp?.type is "Literal"
try literalValue stepExp // ignore invalid literals
if stepValue <? "number"
asc = stepValue > 0

// start needs to be ref'd to compute start <= end, unless we know direction
startRef .= if stepRef then start else maybeRef(start, "start")
Expand All @@ -153,15 +164,16 @@ function forRange(
. stepRef

let ascDec: ASTNode[] = [], ascRef
if stepRef
if stepExp
unless stepRef is stepExp
ascDec = [", ", stepRef, " = ", stepExp]

else if "Literal" is start.type is end.type
else if start?.type is "Literal" is end?.type
// @ts-ignore Allow comparison of any literal values, as in JS
asc = literalValue(start) <= literalValue(end)
if "StringLiteral" is start.subtype is end.subtype
startRef = literalValue(start).charCodeAt(0).toString()
endRef = literalValue(end).charCodeAt(0).toString()
startRef = (literalValue(start) as string).charCodeAt(0).toString()
endRef = (literalValue(end) as string).charCodeAt(0).toString()

else
ascRef = makeRef("asc")
Expand All @@ -171,7 +183,7 @@ function forRange(
if forDeclaration?.declare // var/let/const declaration of variable
if forDeclaration.declare.token is "let"
varName := forDeclaration.children.splice(1) // strip let
varAssign = [...insertTrimmingSpace(varName, ""), " = "]
varAssign = [...trimFirstSpace(varName), " = "]
varLet = [",", ...varName, " = ", counterRef]
else // const or var: put inside loop
// TODO: missing indentation
Expand All @@ -192,14 +204,14 @@ function forRange(
: [counterRef, " < ", endRef, " : ", counterRef, " > ", endRef]

condition :=
infinite ? [] :
infinite or stepValue is 0 ? [] :
asc? ? (asc ? counterPart[0...3] : counterPart[4..]) :
stepRef ? [stepRef, " !== 0 && (", stepRef, " > 0 ? ", ...counterPart, ")"] :
[ascRef, " ? ", ...counterPart]

increment :=
stepRef === "1" ? [...varAssign, "++", counterRef] :
stepRef === "-1" ? [...varAssign, "--", counterRef] :
stepValue is +1 ? [...varAssign, "++", counterRef] :
stepValue is -1 ? [...varAssign, "--", counterRef] :
stepRef
? [...varAssign, counterRef, " += ", stepRef]
: ascRef
Expand All @@ -213,22 +225,25 @@ function forRange(
}

function processForInOf($0: [
awaits: ASTNode,
eachOwn: undefined | [ASTLeaf, ASTNode],
open: ASTNode,
declaration: ASTNode,
declaration2: [ws1: ASTNode, comma: ASTLeaf, ws2: ASTNode, decl2: ASTNode],
ws: ASTNode,
inOf: ASTLeaf,
exp: ASTNodeBase,
step: ASTNode,
close: ASTNode
], getRef)
awaits: ASTNode
eachOwn: [ASTLeaf, ASTNode] | undefined
open: ASTLeaf
declaration: ASTNode
declaration2: [ws1: ASTNode, comma: ASTLeaf, ws2: ASTNode, decl2: ASTNode]
ws: ASTNode
inOf: ASTLeaf
exp: ASTNodeBase
step: [Whitespace, ASTLeaf, ASTNode]
close: ASTLeaf
])
[awaits, eachOwn, open, declaration, declaration2, ws, inOf, exp, step, close] .= $0

if exp.type is "RangeExpression" and inOf.token is "of" and !declaration2
// TODO: add support for `declaration2` to efficient `forRange`
return forRange(open, declaration, exp, step, close)
return forRange
open, declaration, exp
step and prepend(trimFirstSpace(step[0]), trimFirstSpace(step[2])) // omit "by" token
close
else if step
throw new Error("for..of/in cannot use 'by' except with range literals")

Expand All @@ -248,24 +263,24 @@ function processForInOf($0: [
if declaration2
const [, , ws2, decl2] = declaration2 // strip __ Comma __
blockPrefix.push(["", [
insertTrimmingSpace(ws2, ""), decl2, " = ", counterRef
trimFirstSpace(ws2), decl2, " = ", counterRef
], ";"])
assignmentNames.push(...decl2.names)

expRefDec := (expRef !== exp)
// Trim a single leading space if present
? [insertTrimmingSpace(expRef, " "), " = ", insertTrimmingSpace(exp, ""), ", "]
? [trimFirstSpace(expRef), " = ", trimFirstSpace(exp), ", "]
: []

blockPrefix.push ["", {
type: "Declaration"
children: [declaration, " = ", insertTrimmingSpace(expRef, ""), "[", counterRef, "]"]
children: [declaration, " = ", trimFirstSpace(expRef), "[", counterRef, "]"]
names: assignmentNames
}, ";"]

declaration =
type: "Declaration"
children: ["let ", ...expRefDec, counterRef, " = 0, ", lenRef, " = ", insertTrimmingSpace(expRef, ""), ".length"]
children: ["let ", ...expRefDec, counterRef, " = 0, ", lenRef, " = ", trimFirstSpace(expRef), ".length"]
names: []

condition := [counterRef, " < ", lenRef, "; "]
Expand Down Expand Up @@ -317,7 +332,7 @@ function processForInOf($0: [
return {
declaration
blockPrefix
children: [awaits, eachOwnError, open, declaration, ws, inOf, expRef ?? exp, step, close] // omit declaration2, replace eachOwn with eachOwnError, replace exp with expRef
children: [awaits, eachOwnError, open, declaration, ws, inOf, expRef ?? exp, close] // omit declaration2, replace eachOwn with eachOwnError, replace exp with expRef
}

let ws2: ASTNode?, decl2: ASTNode?
Expand All @@ -333,7 +348,7 @@ function processForInOf($0: [
}
blockPrefix.push ["", {
type: "Declaration"
children: [insertTrimmingSpace(ws2, ""), decl2, " = ", counterRef, "++"]
children: [trimFirstSpace(ws2), decl2, " = ", counterRef, "++"]
names: decl2.names
}, ";"]

Expand All @@ -350,12 +365,12 @@ function processForInOf($0: [
children: [" ", expRef, " =", exp]
// for own..in
if own
const hasPropRef = getRef("hasProp")
blockPrefix.push ["", ["if (!", hasPropRef, "(", insertTrimmingSpace(expRef, ""), ", ", insertTrimmingSpace(pattern, ""), ")) continue"], ";"]
hasPropRef := getHelperRef("hasProp")
blockPrefix.push ["", ["if (!", hasPropRef, "(", trimFirstSpace(expRef), ", ", trimFirstSpace(pattern), ")) continue"], ";"]
if decl2
blockPrefix.push ["", {
type: "Declaration"
children: [insertTrimmingSpace(ws2, ""), decl2, " = ", insertTrimmingSpace(expRef, ""), "[", insertTrimmingSpace(pattern, ""), "]"]
children: [trimFirstSpace(ws2), decl2, " = ", trimFirstSpace(expRef), "[", trimFirstSpace(pattern), "]"]
names: decl2.names
}, ";"]

Expand All @@ -364,7 +379,7 @@ function processForInOf($0: [

return {
declaration,
children: [awaits, eachOwnError, open, declaration, ws, inOf, exp, step, close], // omit declaration2, replace each with eachOwnError
children: [awaits, eachOwnError, open, declaration, ws, inOf, exp, close], // omit declaration2, replace each with eachOwnError
blockPrefix,
hoistDec,
}
Expand Down
15 changes: 9 additions & 6 deletions source/parser/types.civet
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ export type OtherNode =
| PinProperty
| Placeholder
| PropertyAccess
| RangeDots
| RangeExpression
| ReturnValue
| SliceExpression
Expand Down Expand Up @@ -158,6 +157,9 @@ export type ASTLeaf =
parent?: Parent
children?: never

export type ASTLeafWithType<T extends string> =
Exclude<ASTLeaf, "type"> & { type: T }

export type CommentNode =
type: "Comment"
$loc: Loc
Expand All @@ -172,6 +174,7 @@ export type BinaryOp = (string &
assoc?: never
type?: undefined
) | (ASTLeaf &
type?: undefined
special?: true
// The following are allowed only when special is true:
prec?: string | number | undefined
Expand Down Expand Up @@ -951,13 +954,13 @@ export type FieldDefinition
export type Literal =
type: "Literal"
subtype?: "NumericLiteral" | "StringLiteral"
children: Children & [ LiteralContentNode ]
children: Children & LiteralContentNode[]
parent?: Parent
raw: string

export type LiteralContentNode = ASTLeaf & {
type?: "NumericLiteral" | "StringLiteral"
}
export type LiteralContentNode =
| ASTLeaf
| ASTLeafWithType "NumericLiteral" | "StringLiteral"

export type RangeExpression
type: "RangeExpression"
Expand Down Expand Up @@ -1053,7 +1056,7 @@ export type TypeLiteral =
children: Children
parent?: Parent

export type VoidType = ASTLeaf & type: "VoidType"
export type VoidType = ASTLeafWithType "VoidType"

export type TypeLiteralNode = ASTLeaf | VoidType

Expand Down
Loading

0 comments on commit bec932c

Please sign in to comment.