Skip to content

Commit

Permalink
Merge pull request #1476 from DanielXMoore/range-inequalities
Browse files Browse the repository at this point in the history
Ranges and slices with inequalities such as `[a<..<b]`
  • Loading branch information
edemaine authored Oct 20, 2024
2 parents 1e351b5 + 8b4a293 commit c7f5ace
Show file tree
Hide file tree
Showing 11 changed files with 653 additions and 107 deletions.
41 changes: 39 additions & 2 deletions civet.dev/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,8 @@ You can also omit the name of the rest component:

### Range Literals

`[x..y]` includes `x` and `y`, while `[x...y]` includes `x` but not `y`.
`[x..y]` includes `x` and `y`, while `[x...y]` includes `x` but not `y`
(as in Ruby and CoffeeScript).

<Playground>
letters := ['a'..'f']
Expand All @@ -416,6 +417,22 @@ indices := [0...array.length]

An infinite range `[x..]` is supported when [looping](#range-loop).

Alternatively, `..` can explicitly exclude endpoints with `<` or `>`,
or include endpoints with `<=` or `>=`. These also control loop direction,
whereas `[a..b]` determines direction based on whether `a <= b`.

<Playground>
indices := [0..<array.length]
</Playground>

<Playground>
reversed := [array.length>..0]
</Playground>

<Playground>
increasing := [a..<=b]
</Playground>

### Array/String Slicing

`[i..j]` includes `i` and `j`, while `[i...j]` includes `i` but not `j`.
Expand All @@ -428,6 +445,12 @@ end := numbers[-2..]
numbers[1...-1] = []
</Playground>

Alternatively, you can exclude or include endpoints using `..` with `<` or `<=`:

<Playground>
strict := numbers[first<..<last]
</Playground>

## Strings

Strings can span multiple lines:
Expand All @@ -437,7 +460,6 @@ console.log "Hello,
world!"
</Playground>


### Triple-Quoted Strings

Leading indentation is removed.
Expand Down Expand Up @@ -1775,6 +1797,21 @@ for i of [1..]
attempt i
</Playground>
You can control loop direction and include or exclude endpoints using
`..` with `<=`/`>=` or `<`/`>`:
<Playground>
for i of [first..<=last]
console.log array[i]
</Playground>
<Playground>
for i of [left<..<right]
console.log array[i]
</Playground>
See also [range literals](#range-literals).
### Until Loop
<Playground>
Expand Down
6 changes: 4 additions & 2 deletions source/generate.civet
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type { ASTNode, ASTError } from './parser/types.civet'
type { ASTNode } from './parser/types.civet'
{ removeParentPointers } from './parser/util.civet'
{ ParseError } from './parser.hera'

Expand All @@ -8,7 +8,7 @@ export type Options =
data: { srcLine: number, srcColumn: number, srcOffset: number }
js?: boolean
filename?: string
errors?: ASTError[]
errors?: ParseError[]

function stringify(node: ASTNode): string
try
Expand Down Expand Up @@ -47,6 +47,8 @@ function gen(root: ASTNode, options: Options): string
column: number | string .= '?'
let offset: number?
if sourceMap := options.sourceMap
if node.$loc?
sourceMap.updateSourceMap "", node.$loc.pos
// Convert 0-based to 1-based
line = sourceMap.data.srcLine + 1
column = sourceMap.data.srcColumn + 1
Expand Down
129 changes: 61 additions & 68 deletions source/parser.hera
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
processForInOf,
processProgram,
processProgramAsync,
processRangeExpression,
processTryBlock,
processUnaryExpression,
processUnaryNestedExpression,
Expand Down Expand Up @@ -1494,20 +1495,28 @@ MemberBracketContent
}

SliceParameters
Expression:start __:ws ( DotDotDot / DotDot ):sep Expression?:end ->
const inclusive = sep.token === ".."

Expression:start __:ws RangeDots:dots Expression?:end ->
let children
if (!dots.left.inclusive) {
start = ["1 + ", makeLeftHandSideExpression(start)]
}
if (end) {
const inc = []
if (inclusive) {
if (dots.right.inclusive) {
end = ["1 + ", makeLeftHandSideExpression(end)]
inc./**/push(" || 1/0")
}
children = [start, [...ws, {...sep, token: ", "}], [end, ...inc]]
children = [start, [...ws, dots.children[0], {token: ", ", $loc: dots.$loc}], [dots.children[1], end, ...inc]]
} else {
children = [start, ws]
}
if (dots.increasing === false) {
children.push({
type: "Error",
message: "Slice range cannot be decreasing",
$loc: dots.$loc,
})
}

return {
type: "SliceParameters",
Expand Down Expand Up @@ -2891,75 +2900,56 @@ _ArrayLiteral
}
NestedBulletedArray

RangeExpression
Expression:s __:ws ( DotDotDot / DotDot ):range Expression:e ->
const inclusive = range.token === ".."
range.token = ","

if (s.type === "Literal" && e.type === "Literal") {
const start = literalValue(s)
const end = literalValue(e)

if (typeof start !== typeof end) {
throw new Error("Range start and end must be of the same type")
RangeDots
DotDotDot ->
return { ...$1,
type: "RangeDots",
left: { inclusive: true, raw: "" },
right: { inclusive: false, raw: "." },
increasing: undefined,
children: [],
}
RangeEnd:left _?:ws1 DotDot:dots _?:ws2 RangeEnd:right ->
// Inherit increasing flag from either side
const increasing = left.increasing ?? right.increasing
if (left.increasing != null && right.increasing != null &&
left.increasing !== right.increasing) {
const error = {
type: "Error",
message: `${left.raw}..${right.raw} uses inconsistent < vs. >`,
$loc: dots.$loc,
}

if (typeof start === "string") {
if (start.length !== 1 || end.length !== 1) {
throw new Error("String range start and end must be a single character")
}

const startCode = start.charCodeAt(0)
const endCode = end.charCodeAt(0)
const step = startCode < endCode ? 1 : -1

const length = Math.abs(endCode - startCode) + (inclusive ? 1 : 0)
if (length <= 26) {
return {
type: "RangeExpression",
children: ["[", Array.from({ length }, (_, i) => JSON.stringify(String.fromCharCode(startCode + i * step))).join(", "), "]"],
inclusive,
start: s,
end: e
}
} else {
const inclusiveAdjust = inclusive ? " + 1" : ""
const children = ["((s, e) => {let step = e > s ? 1 : -1; return Array.from({length: Math.abs(e - s)", inclusiveAdjust, "}, (_, i) => String.fromCharCode(s + i * step))})(", startCode.toString(), ws, range, endCode.toString(), ")"]
return {
type: "RangeExpression",
children,
inclusive,
start: s,
end: e,
}
}
} else if (typeof start === "number") {
const step = end > start ? 1 : -1

const length = Math.abs(end - start) + (inclusive ? 1 : 0)
if (length <= 20) {
// Use array of literal values
return {
type: "RangeExpression",
children: ["[", Array.from({ length }, (_, i) => start + i * step).join(", "), "]"],
inclusive,
start: s,
end: e,
}
}
return {
...dots, left, right, increasing, error, type: "RangeDots",
children: [error]
}
}
return {
...dots, left, right, increasing, type: "RangeDots",
children: [ws1, ws2]
}

const inclusiveAdjust = inclusive ? " + 1" : ""
const children = ["((s, e) => {let step = e > s ? 1 : -1; return Array.from({length: Math.abs(e - s)", inclusiveAdjust, "}, (_, i) => s + i * step)})(", s, ws, range, e, ")"]

RangeEnd
/([<>])(=?)|([≤≥])/ ->
let dir = $1, equal = $2, unicode = $3
if (unicode) {
equal = "="
if (unicode === "≤") {
dir = "<"
} else if (unicode === "≥") {
dir = ">"
}
}
return {
type: "RangeExpression",
children,
inclusive,
start: s,
end: e,
increasing: dir === "<",
inclusive: equal === "=",
raw: $0,
}
"" -> { increasing: undefined, inclusive: true, raw: "" }

RangeExpression
Expression:start __:ws RangeDots:range Expression:end ->
return processRangeExpression(start, ws, range, end)

# NOTE: [x..] range to infinity, valid only in for loops
Expression:s __:ws DotDot &( __ CloseBracket ) ->
Expand All @@ -2975,6 +2965,9 @@ RangeExpression
name: "Infinity",
children: ["Infinity"],
},
left: { inclusive: true, raw: "" },
right: { inclusive: true, raw: "" },
increasing: true,
}

ArrayLiteralContent
Expand Down
Loading

0 comments on commit c7f5ace

Please sign in to comment.