Skip to content

Commit 066f831

Browse files
committed
Add comparison operators
1 parent cac7233 commit 066f831

8 files changed

Lines changed: 310 additions & 7 deletions

File tree

Help/expressions.md

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,18 @@ Operators are used in conjunction with individual values to perform calculations
1414
5 + 3 * 4
1515
```
1616

17-
ShapeScript supports common [infix](https://en.wikipedia.org/wiki/Infix_notation) math operators such as +, -, * and /. Unary + and - are also supported:
17+
ShapeScript supports all the standard [infix](https://en.wikipedia.org/wiki/Infix_notation) arithmetic operators:
18+
19+
Symbol | Name | Function
20+
:------------- | :-------------------- | :--------------------------------------------------------------------
21+
+ | plus | Adds the left and right values
22+
‐ | minus | Subtracts the right value from the left value
23+
* | times | Multiplies the left value by the right value
24+
/ | divide | Divides the left value by the right value
25+
26+
<br>
27+
28+
Unary + and - are also supported:
1829

1930
```swift
2031
-5 * +7
@@ -41,6 +52,40 @@ Whereas this expression would be interpreted as a 2D vector of 5 and -1:
4152
5 -1
4253
```
4354

55+
## Equality and Comparison
56+
57+
In addition to the standard arithmetic operators, ShapeScript also has equality and comparison operators, which can be used in [conditional logic](control-flow.md#if-else). The following infix comparison operators are supported:
58+
59+
Symbol | Name | Function
60+
:------------- | :-------------------- |:--------------------------------------
61+
= | equal | Compares two values and returns `true` if they are equal
62+
<> | not equal | Compares two values and returns `false` if they are equal
63+
< | less than | Returns `true` if the left value is less than the value on the right
64+
<= | less than or equal | Returns `true` if the left value is less than or equal to the right
65+
&gt; | greater than | Returns `true` if the left value is greater than the value on the right
66+
&gt;= | greater than or equal | Returns `true` if the left value is greater than or equal to the right
67+
68+
<br>
69+
70+
**Note:** You may have used other languages where `=` is written as `==`. This is generally because in such languages the `=` operator is used for assignment, and re-using the same symbol would cause ambiguity. This is not a problem in ShapeScript.
71+
72+
While these operators are typically used with numeric inputs, the *output* is a boolean value (`true` or `false`). These values are most commonly used in conjunction with with the `if/else` control flow statement. For example:
73+
74+
```swift
75+
if rnd > 0.5 {
76+
print "heads"
77+
} else {
78+
print "tails"
79+
}
80+
```
81+
82+
But they can also be assigned to a symbol and passed around:
83+
84+
```swift
85+
define averageColor (color.red + color.green + color.blue) / 3
86+
define isBrightColor averageColor >= 0.5
87+
print isBrightColor // true or false
88+
```
4489

4590
## Members
4691

@@ -57,7 +102,7 @@ print yComponent 0.2
57102
Like other operators, the dot operator can be used as part of a larger expression:
58103

59104
```swift
60-
define color 1 0.5 0.2
105+
color 1 0.5 0.2
61106
define averageColor (color.red + color.green + color.blue) / 3
62107
print averageColor // 0.5667
63108
```

Help/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ ShapeScript Help
6464
- [Symbols](symbols.md)
6565
- [Expressions](expressions.md)
6666
- [Operators](expressions.md#operators)
67+
- [Equality and Comparison](expressions.md#equality-and-comparison)
6768
- [Members](expressions.md#members)
6869
- [Ranges](expressions.md#ranges)
6970
- [Functions](functions.md)

ShapeScript/Interpreter.swift

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ enum Value {
395395
case let .path(path): return path
396396
case let .mesh(mesh): return mesh
397397
case let .point(point): return point
398+
case let .tuple(values) where values.count == 1: return values[0].value
398399
case let .tuple(values): return values.map { $0.value }
399400
case let .range(range): return range
400401
case let .bounds(bounds): return bounds
@@ -435,6 +436,13 @@ enum Value {
435436
}
436437
}
437438

439+
var tupleValue: [AnyHashable] {
440+
if case let .tuple(values) = self {
441+
return values.map { $0.value }
442+
}
443+
return [value]
444+
}
445+
438446
var type: ValueType {
439447
switch self {
440448
case .color: return .color
@@ -1216,6 +1224,14 @@ extension Expression {
12161224
)
12171225
}
12181226
return .range(value)
1227+
case let .infix(lhs, .equal, rhs):
1228+
let lhs = try lhs.evaluate(in: context)
1229+
let rhs = try rhs.evaluate(in: context)
1230+
return .boolean(lhs.value == rhs.value)
1231+
case let .infix(lhs, .unequal, rhs):
1232+
let lhs = try lhs.evaluate(in: context)
1233+
let rhs = try rhs.evaluate(in: context)
1234+
return .boolean(lhs.value != rhs.value)
12191235
case let .infix(lhs, op, rhs):
12201236
let lhs = try lhs.evaluate(as: .number, for: String(op.rawValue), index: 0, in: context)
12211237
let rhs = try rhs.evaluate(as: .number, for: String(op.rawValue), index: 1, in: context)
@@ -1228,7 +1244,15 @@ extension Expression {
12281244
return .number(lhs.doubleValue * rhs.doubleValue)
12291245
case .divide:
12301246
return .number(lhs.doubleValue / rhs.doubleValue)
1231-
case .to, .step:
1247+
case .lt:
1248+
return .boolean(lhs.doubleValue < rhs.doubleValue)
1249+
case .gt:
1250+
return .boolean(lhs.doubleValue > rhs.doubleValue)
1251+
case .lte:
1252+
return .boolean(lhs.doubleValue <= rhs.doubleValue)
1253+
case .gte:
1254+
return .boolean(lhs.doubleValue >= rhs.doubleValue)
1255+
case .to, .step, .equal, .unequal:
12321256
preconditionFailure("\(op.rawValue) should be handled by earlier case")
12331257
}
12341258
case let .member(expression, member):

ShapeScript/Lexer.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ public enum InfixOperator: String {
5959
case minus = "-"
6060
case times = "*"
6161
case divide = "/"
62+
// Comparison operators
63+
case lt = "<"
64+
case gt = ">"
65+
case lte = "<="
66+
case gte = ">="
67+
case equal = "="
68+
case unequal = "<>"
6269
// Range operators
6370
case to, step
6471
}

ShapeScript/Parser.swift

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ private extension ArraySlice where Element == Token {
383383
)
384384
}
385385

386-
mutating func readExpression() throws -> Expression? {
386+
mutating func readStep() throws -> Expression? {
387387
guard let lhs = try readRange() else {
388388
return nil
389389
}
@@ -407,6 +407,27 @@ private extension ArraySlice where Element == Token {
407407
)
408408
}
409409

410+
mutating func readComparison() throws -> Expression? {
411+
guard let lhs = try readStep() else {
412+
return nil
413+
}
414+
guard case let .infix(op) = nextToken.type, [
415+
.lt, .lte, .gt, .gte, .equal, .unequal,
416+
].contains(op) else {
417+
return lhs
418+
}
419+
removeFirst()
420+
let rhs = try require(readSum(), as: "operand")
421+
return Expression(
422+
type: .infix(lhs, op, rhs),
423+
range: lhs.range.lowerBound ..< rhs.range.upperBound
424+
)
425+
}
426+
427+
mutating func readExpression() throws -> Expression? {
428+
try readComparison()
429+
}
430+
410431
mutating func readExpressions(allowLinebreaks: Bool = false) throws -> Expression? {
411432
var expressions = [Expression]()
412433
while var expression = try readExpression() {

ShapeScript/StandardLibrary.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ extension Dictionary where Key == String, Value == Symbol {
6666

6767
static let materials: Symbols = color + [
6868
"opacity": .property(.number, { parameter, context in
69-
context.material.opacity = (parameter.value as! Double) * context.opacity
69+
context.material.opacity = parameter.doubleValue * context.opacity
7070
}, { context in
7171
.number(context.material.opacity / context.opacity)
7272
}),
@@ -309,7 +309,7 @@ extension Dictionary where Key == String, Value == Symbol {
309309
}),
310310
// Debug
311311
"print": .command(.tuple) { value, context in
312-
context.debugLog(value.value as! [AnyHashable])
312+
context.debugLog(value.tupleValue)
313313
return .void
314314
},
315315
])

ShapeScriptTests/InterpreterTests.swift

Lines changed: 180 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1707,7 +1707,7 @@ class InterpreterTests: XCTestCase {
17071707
}
17081708
}
17091709

1710-
// MARK: Functions
1710+
// MARK: Math functions
17111711

17121712
func testInvokeMonadicFunction() {
17131713
let program = "print cos pi"
@@ -1871,6 +1871,185 @@ class InterpreterTests: XCTestCase {
18711871
XCTAssertEqual(delegate.log, [2])
18721872
}
18731873

1874+
// MARK: Numeric comparison
1875+
1876+
func testGT() {
1877+
let program = "print 5 > 1"
1878+
let delegate = TestDelegate()
1879+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
1880+
XCTAssertEqual(delegate.log, [true])
1881+
}
1882+
1883+
func testGT2() {
1884+
let program = "print 5 > 6"
1885+
let delegate = TestDelegate()
1886+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
1887+
XCTAssertEqual(delegate.log, [false])
1888+
}
1889+
1890+
func testGT3() {
1891+
let program = "print 5 > 5"
1892+
let delegate = TestDelegate()
1893+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
1894+
XCTAssertEqual(delegate.log, [false])
1895+
}
1896+
1897+
func testGTE() {
1898+
let program = "print 2 >= 1"
1899+
let delegate = TestDelegate()
1900+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
1901+
XCTAssertEqual(delegate.log, [true])
1902+
}
1903+
1904+
func testGTE2() {
1905+
let program = "print 2 >= 5"
1906+
let delegate = TestDelegate()
1907+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
1908+
XCTAssertEqual(delegate.log, [false])
1909+
}
1910+
1911+
func testGTE3() {
1912+
let program = "print -2 >= -2"
1913+
let delegate = TestDelegate()
1914+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
1915+
XCTAssertEqual(delegate.log, [true])
1916+
}
1917+
1918+
func testLT() {
1919+
let program = "print 1 < 2"
1920+
let delegate = TestDelegate()
1921+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
1922+
XCTAssertEqual(delegate.log, [true])
1923+
}
1924+
1925+
func testLT2() {
1926+
let program = "print 5 < 4"
1927+
let delegate = TestDelegate()
1928+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
1929+
XCTAssertEqual(delegate.log, [false])
1930+
}
1931+
1932+
func testLT3() {
1933+
let program = "print -2 < -2"
1934+
let delegate = TestDelegate()
1935+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
1936+
XCTAssertEqual(delegate.log, [false])
1937+
}
1938+
1939+
func testLTE() {
1940+
let program = "print 1 <= 2"
1941+
let delegate = TestDelegate()
1942+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
1943+
XCTAssertEqual(delegate.log, [true])
1944+
}
1945+
1946+
func testLTE2() {
1947+
let program = "print 5 <= 4"
1948+
let delegate = TestDelegate()
1949+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
1950+
XCTAssertEqual(delegate.log, [false])
1951+
}
1952+
1953+
func testLTE3() {
1954+
let program = "print -2 <= -2"
1955+
let delegate = TestDelegate()
1956+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
1957+
XCTAssertEqual(delegate.log, [true])
1958+
}
1959+
1960+
// MARK: Equality
1961+
1962+
func testNumbersEqual() {
1963+
let program = "print 5 = 5"
1964+
let delegate = TestDelegate()
1965+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
1966+
XCTAssertEqual(delegate.log, [true])
1967+
}
1968+
1969+
func testNumbersEqual2() {
1970+
let program = "print 5 = 2"
1971+
let delegate = TestDelegate()
1972+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
1973+
XCTAssertEqual(delegate.log, [false])
1974+
}
1975+
1976+
func testNumbersUnequal() {
1977+
let program = "print 5 <> 5"
1978+
let delegate = TestDelegate()
1979+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
1980+
XCTAssertEqual(delegate.log, [false])
1981+
}
1982+
1983+
func testNumbersUnequal2() {
1984+
let program = "print 5 <> 4"
1985+
let delegate = TestDelegate()
1986+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
1987+
XCTAssertEqual(delegate.log, [true])
1988+
}
1989+
1990+
func testStringsEqual() {
1991+
let program = "print \"foo\" = \"foo\""
1992+
let delegate = TestDelegate()
1993+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
1994+
XCTAssertEqual(delegate.log, [true])
1995+
}
1996+
1997+
func testStringsEqual2() {
1998+
let program = "print \"foo\" = \"bar\""
1999+
let delegate = TestDelegate()
2000+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
2001+
XCTAssertEqual(delegate.log, [false])
2002+
}
2003+
2004+
func testStringsUnequal() {
2005+
let program = "print \"foo\" <> \"foo\""
2006+
let delegate = TestDelegate()
2007+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
2008+
XCTAssertEqual(delegate.log, [false])
2009+
}
2010+
2011+
func testStringsUnequal2() {
2012+
let program = "print \"foo\" <> \"bar\""
2013+
let delegate = TestDelegate()
2014+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
2015+
XCTAssertEqual(delegate.log, [true])
2016+
}
2017+
2018+
func testTuplesEqual() {
2019+
let program = "print 1 2 3 = 1 2 3"
2020+
let delegate = TestDelegate()
2021+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
2022+
XCTAssertEqual(delegate.log, [1, 2, false, 2, 3])
2023+
}
2024+
2025+
func testTuplesEqual2() {
2026+
let program = "print (1 2 3) = (1 2 3)"
2027+
let delegate = TestDelegate()
2028+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
2029+
XCTAssertEqual(delegate.log, [true])
2030+
}
2031+
2032+
func testFunctionResultsEqual() {
2033+
let program = "print min(1 2) = 1"
2034+
let delegate = TestDelegate()
2035+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
2036+
XCTAssertEqual(delegate.log, [true])
2037+
}
2038+
2039+
func testMismatchedTypesEqual() {
2040+
let program = "print \"foo\" = 5"
2041+
let delegate = TestDelegate()
2042+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
2043+
XCTAssertEqual(delegate.log, [false])
2044+
}
2045+
2046+
func testMismatchedTypesUnequal() {
2047+
let program = "print \"foo\" <> 5"
2048+
let delegate = TestDelegate()
2049+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
2050+
XCTAssertEqual(delegate.log, [true])
2051+
}
2052+
18742053
// MARK: Member lookup
18752054

18762055
func testTupleVectorLookup() {

0 commit comments

Comments
 (0)