Skip to content

Commit 9488145

Browse files
committed
Convert range expression to separate to/step operators
1 parent aec8d23 commit 9488145

6 files changed

Lines changed: 187 additions & 56 deletions

File tree

Help/expressions.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,20 @@ for i in 0 to 1 step 0.2 {
117117
}
118118
```
119119

120+
The step value for an existing range can be set or overridden later:
121+
122+
```swift
123+
define loops 1 to 5 step 3
124+
125+
for i in loops {
126+
print i // prints 1, 4
127+
}
128+
129+
for i in loops step 2 {
130+
print i // prints 1, 3, 5
131+
}
132+
```
133+
120134
A negative `step` can be used to create a [backwards loop](control-flow.md#looping-backwards):
121135

122136
```swift

ShapeScript/Interpreter.swift

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,6 +1173,25 @@ extension Expression {
11731173
case .plus:
11741174
return .number(value.doubleValue)
11751175
}
1176+
case let .infix(lhs, .to, rhs):
1177+
let start = try lhs.evaluate(as: .number, for: "start value", in: context)
1178+
let end = try rhs.evaluate(as: .number, for: "end value", in: context)
1179+
return .range(RangeValue(from: start.doubleValue, to: end.doubleValue))
1180+
case let .infix(lhs, .step, rhs):
1181+
let rangeValue = try lhs.evaluate(as: .range, for: "range value", in: context)
1182+
let stepValue = try rhs.evaluate(as: .number, for: "step value", in: context)
1183+
let range = rangeValue.value as! RangeValue
1184+
guard let value = RangeValue(
1185+
from: range.start,
1186+
to: range.end,
1187+
step: stepValue.doubleValue
1188+
) else {
1189+
throw RuntimeError(
1190+
.assertionFailure("Step value must be nonzero"),
1191+
at: rhs.range
1192+
)
1193+
}
1194+
return .range(value)
11761195
case let .infix(lhs, op, rhs):
11771196
let lhs = try lhs.evaluate(as: .number, for: String(op.rawValue), index: 0, in: context)
11781197
let rhs = try rhs.evaluate(as: .number, for: String(op.rawValue), index: 1, in: context)
@@ -1185,25 +1204,9 @@ extension Expression {
11851204
return .number(lhs.doubleValue * rhs.doubleValue)
11861205
case .divide:
11871206
return .number(lhs.doubleValue / rhs.doubleValue)
1207+
case .to, .step:
1208+
preconditionFailure("\(op.rawValue) should be handled by earlier case")
11881209
}
1189-
case let .range(from: start, to: end, step: step):
1190-
let start = try start.evaluate(as: .number, for: "start value", in: context)
1191-
let end = try end.evaluate(as: .number, for: "end value", in: context)
1192-
guard let stepParam = step else {
1193-
return .range(RangeValue(from: start.doubleValue, to: end.doubleValue))
1194-
}
1195-
let step = try stepParam.evaluate(as: .number, for: "step value", in: context)
1196-
guard let value = RangeValue(
1197-
from: start.doubleValue,
1198-
to: end.doubleValue,
1199-
step: step.doubleValue
1200-
) else {
1201-
throw RuntimeError(
1202-
.assertionFailure("Step value must be nonzero"),
1203-
at: stepParam.range
1204-
)
1205-
}
1206-
return .range(value)
12071210
case let .member(expression, member):
12081211
var value = try expression.evaluate(in: context)
12091212
if let memberValue = value[member.name] {

ShapeScript/Lexer.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ public enum InfixOperator: String {
5757
case minus = "-"
5858
case times = "*"
5959
case divide = "/"
60+
// Range operators
61+
case to, step
6062
}
6163

6264
public enum TokenType: Equatable {

ShapeScript/Parser.swift

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ public enum ExpressionType: Equatable {
6363
indirect case tuple([Expression])
6464
indirect case prefix(PrefixOperator, Expression)
6565
indirect case infix(Expression, InfixOperator, Expression)
66-
indirect case range(from: Expression, to: Expression, step: Expression?)
6766
indirect case member(Expression, Identifier)
6867
indirect case subexpression(Expression)
6968
}
@@ -328,34 +327,57 @@ private extension ArraySlice where Element == Token {
328327
return lhs
329328
}
330329

331-
mutating func readExpression() throws -> Expression? {
330+
mutating func readSum() throws -> Expression? {
332331
guard var lhs = try readTerm() else {
333332
return nil
334333
}
335-
while case let .infix(op) = nextToken.type {
334+
while case let .infix(op) = nextToken.type, [.plus, .minus].contains(op) {
336335
removeFirst()
337336
let rhs = try require(readTerm(), as: "operand")
338337
lhs = Expression(
339338
type: .infix(lhs, op, rhs),
340339
range: lhs.range.lowerBound ..< rhs.range.upperBound
341340
)
342341
}
342+
return lhs
343+
}
344+
345+
mutating func readRange() throws -> Expression? {
346+
guard let lhs = try readSum() else {
347+
return nil
348+
}
343349
guard case .identifier("to") = nextToken.type else {
344350
return lhs
345351
}
346352
removeFirst()
347-
let rhs = try require(readExpression(), as: "end value")
353+
let rhs = try require(readSum(), as: "end value")
354+
return Expression(
355+
type: .infix(lhs, .to, rhs),
356+
range: lhs.range.lowerBound ..< rhs.range.upperBound
357+
)
358+
}
359+
360+
mutating func readExpression() throws -> Expression? {
361+
guard let lhs = try readRange() else {
362+
return nil
363+
}
348364
guard case .identifier("step") = nextToken.type else {
349-
return Expression(
350-
type: .range(from: lhs, to: rhs, step: nil),
351-
range: lhs.range.lowerBound ..< rhs.range.upperBound
352-
)
365+
return lhs
353366
}
367+
let start = self
354368
removeFirst()
355-
let step = try require(readExpression(), as: "step value")
369+
guard let rhs = try readSum() else {
370+
self = start
371+
return lhs
372+
}
373+
if case .identifier("step") = nextToken.type {
374+
// TODO: should multiple step values actually be permitted?
375+
// TODO: or is there a better error than "unexpected token"?
376+
throw ParserError(.unexpectedToken(nextToken, expected: nil))
377+
}
356378
return Expression(
357-
type: .range(from: lhs, to: rhs, step: step),
358-
range: lhs.range.lowerBound ..< step.range.upperBound
379+
type: .infix(lhs, .step, rhs),
380+
range: lhs.range.lowerBound ..< rhs.range.upperBound
359381
)
360382
}
361383

@@ -391,7 +413,7 @@ private extension ArraySlice where Element == Token {
391413
return try readExpressions().map { .expression($0) }
392414
}
393415
switch nextToken.type {
394-
case .infix, .dot, .identifier("to"):
416+
case .infix, .dot, .identifier("to"), .identifier("step"):
395417
self = start
396418
return try readExpressions().map { .expression($0) }
397419
case .lbrace:

ShapeScriptTests/InterpreterTests.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1460,6 +1460,26 @@ class InterpreterTests: XCTestCase {
14601460
}
14611461
}
14621462

1463+
func testRangeExtendedByStepValue() {
1464+
let program = """
1465+
define range 1 to 5
1466+
print range step 2
1467+
"""
1468+
let delegate = TestDelegate()
1469+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
1470+
XCTAssertEqual(delegate.log, [RangeValue(from: 1, to: 5, step: 2)])
1471+
}
1472+
1473+
func testRangeWithStepExtendedByDifferentStepValue() {
1474+
let program = """
1475+
define range 1 to 5 step 3
1476+
print range step 2
1477+
"""
1478+
let delegate = TestDelegate()
1479+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
1480+
XCTAssertEqual(delegate.log, [RangeValue(from: 1, to: 5, step: 2)])
1481+
}
1482+
14631483
// MARK: For loops
14641484

14651485
func testForLoopWithIndex() {
@@ -1572,6 +1592,36 @@ class InterpreterTests: XCTestCase {
15721592
XCTAssertEqual(delegate.log, [1, 2, 3])
15731593
}
15741594

1595+
func testForLoopWithRangeVariableAndNoIndex() {
1596+
let program = """
1597+
define range 1 to 3
1598+
for range { print "a" }
1599+
"""
1600+
let delegate = TestDelegate()
1601+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
1602+
XCTAssertEqual(delegate.log, ["a", "a", "a"])
1603+
}
1604+
1605+
func testForLoopWithRangeVariableExtendedByStepValue() {
1606+
let program = """
1607+
define range 1 to 5
1608+
for i in range step 2 { print i }
1609+
"""
1610+
let delegate = TestDelegate()
1611+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
1612+
XCTAssertEqual(delegate.log, [1, 3, 5])
1613+
}
1614+
1615+
func testForLoopWithRangeVariableExtendedByStepValueAndNoIndex() {
1616+
let program = """
1617+
define range 1 to 5
1618+
for range step 2 { print "a" }
1619+
"""
1620+
let delegate = TestDelegate()
1621+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
1622+
XCTAssertEqual(delegate.log, ["a", "a", "a"])
1623+
}
1624+
15751625
func testForLoopWithTupleVariable() {
15761626
let program = """
15771627
define values 3 1 4 1 5

ShapeScriptTests/ParserTests.swift

Lines changed: 66 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -252,10 +252,10 @@ class ParserTests: XCTestCase {
252252
type: .define(
253253
Identifier(name: "foo", range: fooRange),
254254
Definition(type: .expression(Expression(
255-
type: .range(
256-
from: Expression(type: .number(1), range: range1),
257-
to: Expression(type: .number(2), range: range2),
258-
step: nil
255+
type: .infix(
256+
Expression(type: .number(1), range: range1),
257+
.to,
258+
Expression(type: .number(2), range: range2)
259259
),
260260
range: range1.lowerBound ..< range2.upperBound
261261
)))
@@ -277,10 +277,17 @@ class ParserTests: XCTestCase {
277277
type: .define(
278278
Identifier(name: "foo", range: fooRange),
279279
Definition(type: .expression(Expression(
280-
type: .range(
281-
from: Expression(type: .number(1), range: range1),
282-
to: Expression(type: .number(5), range: range2),
283-
step: Expression(type: .number(2), range: range3)
280+
type: .infix(
281+
Expression(
282+
type: .infix(
283+
Expression(type: .number(1), range: range1),
284+
.to,
285+
Expression(type: .number(5), range: range2)
286+
),
287+
range: range1.lowerBound ..< range2.upperBound
288+
),
289+
.step,
290+
Expression(type: .number(2), range: range3)
284291
),
285292
range: range1.lowerBound ..< range3.upperBound
286293
)))
@@ -303,15 +310,15 @@ class ParserTests: XCTestCase {
303310
}
304311
}
305312

306-
func testRangeWithMissingStepValue() {
307-
let input = "define foo 1 to 5 step"
313+
func testRangeWithMultipleStepValues() {
314+
let input = "define range 1 to 5 step 1 step 2"
315+
let range = input.range(of: "step", range: input.range(of: "step 2")!)!
308316
XCTAssertThrowsError(try parse(input)) { error in
309317
let error = try? XCTUnwrap(error as? ParserError)
310-
XCTAssertEqual(error?.message, "Unexpected end of file")
311-
XCTAssertEqual(error?.hint, "Expected step value.")
318+
XCTAssertEqual(error?.message, "Unexpected token 'step'")
312319
XCTAssertEqual(error, ParserError(.unexpectedToken(
313-
Token(type: .eof, range: input.endIndex ..< input.endIndex),
314-
expected: "step value"
320+
Token(type: .identifier("step"), range: range),
321+
expected: nil
315322
)))
316323
}
317324
}
@@ -354,14 +361,47 @@ class ParserTests: XCTestCase {
354361
let range = range1.lowerBound ..< range2.upperBound
355362
let identifier = Identifier(name: "foo", range: range1)
356363
XCTAssertEqual(try parse(input), Program(source: input, statements: [
357-
Statement(type: .expression(Expression(type: .range(
358-
from: Expression(type: .identifier(identifier), range: range1),
359-
to: Expression(type: .number(2), range: range2),
360-
step: nil
364+
Statement(type: .expression(Expression(type: .infix(
365+
Expression(type: .identifier(identifier), range: range1),
366+
.to,
367+
Expression(type: .number(2), range: range2)
368+
), range: range)), range: range),
369+
]))
370+
}
371+
372+
func testStepExpressionStatement() {
373+
let input = "foo step 2"
374+
let range1 = input.range(of: "foo")!
375+
let range2 = input.range(of: "2")!
376+
let range = range1.lowerBound ..< range2.upperBound
377+
let identifier = Identifier(name: "foo", range: range1)
378+
XCTAssertEqual(try parse(input), Program(source: input, statements: [
379+
Statement(type: .expression(Expression(type: .infix(
380+
Expression(type: .identifier(identifier), range: range1),
381+
.step,
382+
Expression(type: .number(2), range: range2)
361383
), range: range)), range: range),
362384
]))
363385
}
364386

387+
func testNonStepExpressionStatement() {
388+
let input = "foo step"
389+
let range1 = input.range(of: "foo")!
390+
let range2 = input.range(of: "step")!
391+
let range = range1.lowerBound ..< range2.upperBound
392+
let identifier1 = Identifier(name: "foo", range: range1)
393+
let identifier2 = Identifier(name: "step", range: range2)
394+
XCTAssertEqual(try parse(input), Program(source: input, statements: [
395+
Statement(type: .expression(Expression(
396+
type: .tuple([
397+
Expression(type: .identifier(identifier1), range: range1),
398+
Expression(type: .identifier(identifier2), range: range2),
399+
]),
400+
range: range
401+
)), range: range),
402+
]))
403+
}
404+
365405
// MARK: For loops
366406

367407
func testForLoopWithIndex() {
@@ -376,10 +416,10 @@ class ParserTests: XCTestCase {
376416
type: .forloop(
377417
Identifier(name: "i", range: iRange),
378418
in: Expression(
379-
type: .range(
380-
from: Expression(type: .number(1), range: range1),
381-
to: Expression(type: .number(2), range: range2),
382-
step: nil
419+
type: .infix(
420+
Expression(type: .number(1), range: range1),
421+
.to,
422+
Expression(type: .number(2), range: range2)
383423
),
384424
range: range1.lowerBound ..< range2.upperBound
385425
),
@@ -401,10 +441,10 @@ class ParserTests: XCTestCase {
401441
type: .forloop(
402442
nil,
403443
in: Expression(
404-
type: .range(
405-
from: Expression(type: .number(1), range: range1),
406-
to: Expression(type: .number(2), range: range2),
407-
step: nil
444+
type: .infix(
445+
Expression(type: .number(1), range: range1),
446+
.to,
447+
Expression(type: .number(2), range: range2)
408448
),
409449
range: range1.lowerBound ..< range2.upperBound
410450
),

0 commit comments

Comments
 (0)