Skip to content

Commit a4cafbc

Browse files
committed
Add not command
1 parent 0199bd7 commit a4cafbc

5 files changed

Lines changed: 152 additions & 12 deletions

File tree

Help/expressions.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ Operator | Function
9999
:------------- | :--------------------
100100
and | Compares two values and returns `true` if they are both true
101101
or | Compares two values and returns `true` if either one is true
102+
not | Returns `false` if the expression to the right is true, and `true` if it's false
102103

103104
<br>
104105

@@ -113,8 +114,8 @@ if a and b {
113114
These can be combined into more complex expressions, and used in conjunction with parentheses for disambiguation:
114115

115116
```swift
116-
if a and (b or c) {
117-
print "a was true and either b or c were true"
117+
if (not a) and (b or c) {
118+
print "a was false and either b or c were true"
118119
}
119120
```
120121

ShapeScript/Parser.swift

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -408,20 +408,41 @@ private extension ArraySlice where Element == Token {
408408
}
409409

410410
mutating func readComparison() throws -> Expression? {
411-
guard let lhs = try readStep() else {
411+
let not = nextToken.type == .identifier("not") ? readToken() : nil
412+
guard var lhs = try readStep() else {
412413
return nil
413414
}
414-
guard case let .infix(op) = nextToken.type, [
415+
if case let .infix(op) = nextToken.type, [
415416
.lt, .lte, .gt, .gte, .equal, .unequal,
416-
].contains(op) else {
417-
return lhs
417+
].contains(op) {
418+
removeFirst()
419+
// TODO: should we allow chained comparison operators?
420+
let not = nextToken.type == .identifier("not") ? readToken() : nil
421+
var rhs = try require(readSum(), as: "operand")
422+
if let not = not {
423+
rhs = Expression(type: .tuple([
424+
Expression(
425+
type: .identifier(Identifier(name: "not", range: not.range)),
426+
range: not.range
427+
),
428+
rhs,
429+
]), range: not.range.lowerBound ..< rhs.range.upperBound)
430+
}
431+
lhs = Expression(
432+
type: .infix(lhs, op, rhs),
433+
range: lhs.range.lowerBound ..< rhs.range.upperBound
434+
)
418435
}
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-
)
436+
return not.map {
437+
let not = Expression(
438+
type: .identifier(Identifier(name: "not", range: $0.range)),
439+
range: $0.range
440+
)
441+
return Expression(
442+
type: .tuple([not, lhs]),
443+
range: $0.range.lowerBound ..< lhs.range.upperBound
444+
)
445+
} ?? lhs
425446
}
426447

427448
mutating func readBooleanLogic() throws -> Expression? {

ShapeScript/StandardLibrary.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,9 @@ extension Dictionary where Key == String, Value == Symbol {
233233
static let functions: Symbols = [
234234
"true": .constant(.boolean(true)),
235235
"false": .constant(.boolean(false)),
236+
"not": .command(.boolean) { value, _ in
237+
.boolean(!value.boolValue)
238+
},
236239
// Math functions
237240
"rnd": .command(.void) { _, context in
238241
.number(context.random.next())

ShapeScriptTests/InterpreterTests.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1707,6 +1707,13 @@ class InterpreterTests: XCTestCase {
17071707
}
17081708
}
17091709

1710+
func testIfNot() {
1711+
let program = "if not 3 < 1 { print true }"
1712+
let delegate = TestDelegate()
1713+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
1714+
XCTAssertEqual(delegate.log, [true])
1715+
}
1716+
17101717
// MARK: Math functions
17111718

17121719
func testInvokeMonadicFunction() {
@@ -2087,6 +2094,49 @@ class InterpreterTests: XCTestCase {
20872094
XCTAssertEqual(delegate.log, [true, true, false])
20882095
}
20892096

2097+
func testNotVsComparisonOperators() {
2098+
let program = """
2099+
print not 1 > 3
2100+
print not 1 < 3
2101+
"""
2102+
let delegate = TestDelegate()
2103+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
2104+
XCTAssertEqual(delegate.log, [true, false])
2105+
}
2106+
2107+
func testNotVsEquality() {
2108+
let program = """
2109+
print not true = false
2110+
print not true <> false
2111+
print not 5 = 6
2112+
"""
2113+
let delegate = TestDelegate()
2114+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
2115+
XCTAssertEqual(delegate.log, [true, false, true])
2116+
}
2117+
2118+
func testNotVsBooleanOperators() {
2119+
let program = """
2120+
print not true or false
2121+
print not true or true
2122+
print not true and true
2123+
"""
2124+
let delegate = TestDelegate()
2125+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
2126+
XCTAssertEqual(delegate.log, [false, true, false])
2127+
}
2128+
2129+
func testNotVsParens() {
2130+
let program = """
2131+
print (not true) = false
2132+
print not(true) = false
2133+
print true = (not false)
2134+
"""
2135+
let delegate = TestDelegate()
2136+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
2137+
XCTAssertEqual(delegate.log, [true, true, true])
2138+
}
2139+
20902140
// MARK: Member lookup
20912141

20922142
func testTupleVectorLookup() {

ShapeScriptTests/ParserTests.swift

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,71 @@ class ParserTests: XCTestCase {
105105
]))
106106
}
107107

108+
func testNotOperatorPrecedence() {
109+
let input = "not a = b"
110+
let notRange = input.range(of: "not")!
111+
let aRange = input.range(of: "a")!
112+
let bRange = input.range(of: "b")!
113+
XCTAssertEqual(try parse(input), Program(source: input, statements: [
114+
Statement(
115+
type: .command(
116+
Identifier(name: "not", range: notRange),
117+
Expression(
118+
type: .infix(
119+
Expression(type: .identifier(Identifier(
120+
name: "a",
121+
range: aRange
122+
)), range: aRange),
123+
.equal,
124+
Expression(type: .identifier(Identifier(
125+
name: "b",
126+
range: bRange
127+
)), range: bRange)
128+
),
129+
range: aRange.lowerBound ..< input.endIndex
130+
)
131+
),
132+
range: input.startIndex ..< input.endIndex
133+
),
134+
]))
135+
}
136+
137+
func testNotOperatorPrecedence2() {
138+
let input = "not a = not b"
139+
let notRange = input.range(of: "not")!
140+
let aRange = input.range(of: "a")!
141+
let notRange2 = input.range(of: "not", range: input.range(of: "not b")!)!
142+
let bRange = input.range(of: "b")!
143+
XCTAssertEqual(try parse(input), Program(source: input, statements: [
144+
Statement(
145+
type: .command(
146+
Identifier(name: "not", range: notRange),
147+
Expression(
148+
type: .infix(
149+
Expression(type: .identifier(Identifier(
150+
name: "a",
151+
range: aRange
152+
)), range: aRange),
153+
.equal,
154+
Expression(type: .tuple([
155+
Expression(type: .identifier(Identifier(
156+
name: "not",
157+
range: notRange2
158+
)), range: notRange2),
159+
Expression(type: .identifier(Identifier(
160+
name: "b",
161+
range: bRange
162+
)), range: bRange),
163+
]), range: notRange2.lowerBound ..< bRange.upperBound)
164+
),
165+
range: aRange.lowerBound ..< input.endIndex
166+
)
167+
),
168+
range: input.startIndex ..< input.endIndex
169+
),
170+
]))
171+
}
172+
108173
func testUnterminatedInfixExpression() {
109174
let input = "define foo 1 +"
110175
let range = input.endIndex ..< input.endIndex

0 commit comments

Comments
 (0)