Skip to content

Commit 3b06085

Browse files
committed
Add subscripting
1 parent 4fe54a9 commit 3b06085

18 files changed

Lines changed: 592 additions & 243 deletions

ShapeScript/Interpreter.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public func evaluate(
4141
public enum RuntimeErrorType: Error, Equatable {
4242
case unknownSymbol(String, options: [String])
4343
case unknownMember(String, of: String, options: [String])
44+
case invalidIndex(Double, range: Range<Int>)
4445
case unknownFont(String, options: [String])
4546
case typeMismatch(for: String, index: Int, expected: String, got: String)
4647
case forwardReference(String)
@@ -77,6 +78,8 @@ public extension RuntimeError {
7778
return "Unexpected symbol '\(name)'"
7879
case let .unknownMember(name, type, _):
7980
return "Unknown \(type) member property '\(name)'"
81+
case let .invalidIndex(index, _):
82+
return "Index \(index.logDescription) out of bounds"
8083
case let .unknownFont(name, _):
8184
return name.isEmpty ? "Font name cannot be blank" : "Unknown font '\(name)'"
8285
case .typeMismatch:
@@ -130,6 +133,7 @@ public extension RuntimeError {
130133
.forwardReference,
131134
.unexpectedArgument,
132135
.missingArgument,
136+
.invalidIndex,
133137
.unusedValue,
134138
.assertionFailure,
135139
.fileNotFound,
@@ -185,6 +189,8 @@ public extension RuntimeError {
185189
return hint
186190
case .unknownMember:
187191
return suggestion.map { "Did you mean '\($0)'?" }
192+
case let .invalidIndex(_, range: range):
193+
return range.isEmpty ? nil : "Valid range is \(range.lowerBound) to \(range.upperBound - 1)."
188194
case .unknownFont:
189195
if let suggestion = suggestion {
190196
return "Did you mean '\(suggestion)'?"
@@ -266,6 +272,7 @@ public extension RuntimeError {
266272
.circularImport,
267273
.unknownSymbol,
268274
.unknownMember,
275+
.invalidIndex,
269276
.unknownFont:
270277
return nil
271278
}
@@ -1384,6 +1391,32 @@ extension Expression {
13841391
of: value.errorDescription,
13851392
options: value.members
13861393
), at: member.range)
1394+
case let .subscript(lhs, rhs):
1395+
let value = try lhs.evaluate(in: context)
1396+
let index = try rhs.evaluate(in: context)
1397+
switch index.as(.union([.number, .string])) {
1398+
case let .number(number)?:
1399+
let index = Int(truncating: number as NSNumber)
1400+
guard let member = value[index] else {
1401+
throw RuntimeError(.invalidIndex(number, range: value.indices), at: rhs.range)
1402+
}
1403+
return member
1404+
case let .string(key)?:
1405+
guard let member = value[key, context.isCancelled] else {
1406+
throw RuntimeError(.unknownMember(
1407+
index.logDescription,
1408+
of: value.errorDescription,
1409+
options: value.members
1410+
), at: rhs.range)
1411+
}
1412+
return member
1413+
default:
1414+
throw RuntimeError(.typeMismatch(
1415+
for: "index",
1416+
expected: .union([.number, .string]),
1417+
got: index.type
1418+
), at: rhs.range)
1419+
}
13871420
case let .import(expression):
13881421
let pathValue = try expression.evaluate(
13891422
as: .string,

ShapeScript/Lexer.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ public func tokenize(_ input: String) throws -> [Token] {
4242
spaceBefore = characters.skipWhitespaceAndComments()
4343
if !spaceBefore, let lastTokenType = tokens.last?.type {
4444
switch lastTokenType {
45-
case .infix, .prefix, .dot, .lparen, .lbrace, .call, .linebreak:
45+
case .infix, .prefix, .dot, .lparen, .lbrace, .call,
46+
.lbracket, .subscript, .linebreak:
4647
spaceBefore = true
47-
case .identifier, .keyword, .hexColor,
48-
.number, .string, .rbrace, .rparen, .eof:
48+
case .identifier, .keyword, .hexColor, .number,
49+
.string, .rbrace, .rparen, .rbracket, .eof:
4950
break
5051
}
5152
}
@@ -106,8 +107,11 @@ public enum TokenType: Equatable {
106107
case rbrace
107108
case lparen
108109
case rparen
110+
case lbracket
111+
case rbracket
109112
case dot
110113
case call
114+
case `subscript`
111115
case eof
112116
}
113117

@@ -388,6 +392,8 @@ private extension Substring {
388392
case "}": return .rbrace
389393
case "(": return spaceBefore ? .lparen : .call
390394
case ")": return .rparen
395+
case "[": return spaceBefore ? .lbracket : .subscript
396+
case "]": return .rbracket
391397
case "." where !spaceBefore:
392398
if let next = first, !next.isWhitespace, !next.isLinebreak {
393399
return .dot

ShapeScript/Members.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,4 +367,43 @@ extension Value {
367367
return nil
368368
}
369369
}
370+
371+
var indices: Range<Int> {
372+
switch self {
373+
case .vector, .size:
374+
return 0 ..< 3
375+
case .color:
376+
return 0 ..< 4
377+
case let .tuple(values):
378+
return values.startIndex ..< values.endIndex
379+
case .boolean, .texture, .number, .radians, .halfturns, .material, .rotation,
380+
.string, .text, .path, .mesh, .polygon, .point, .range, .bounds, .object:
381+
return 0 ..< 0
382+
}
383+
}
384+
385+
subscript(index: Int) -> Value? {
386+
switch self {
387+
case let .vector(vector), let .size(vector):
388+
switch index {
389+
case 0: return .number(vector.x)
390+
case 1: return .number(vector.y)
391+
case 2: return .number(vector.z)
392+
default: return nil
393+
}
394+
case let .color(color):
395+
switch index {
396+
case 0: return .number(color.r)
397+
case 1: return .number(color.g)
398+
case 2: return .number(color.b)
399+
case 3: return .number(color.a)
400+
default: return nil
401+
}
402+
case let .tuple(values):
403+
return values.indices.contains(index) ? values[index] : nil
404+
case .boolean, .texture, .number, .radians, .halfturns, .material, .rotation,
405+
.string, .text, .path, .mesh, .polygon, .point, .range, .bounds, .object:
406+
return nil
407+
}
408+
}
370409
}

ShapeScript/Parser.swift

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public enum ExpressionType: Equatable {
6767
indirect case prefix(PrefixOperator, Expression)
6868
indirect case infix(Expression, InfixOperator, Expression)
6969
indirect case member(Expression, Identifier)
70+
indirect case `subscript`(Expression, Expression)
7071
indirect case `import`(Expression)
7172
}
7273

@@ -188,6 +189,8 @@ private extension TokenType {
188189
case .rbrace: return "closing brace"
189190
case .lparen, .call: return "opening paren"
190191
case .rparen: return "closing paren"
192+
case .lbracket, .subscript: return "opening bracket"
193+
case .rbracket: return "closing bracket"
191194
case .dot: return "dot"
192195
case .eof: return "end of file"
193196
}
@@ -461,18 +464,32 @@ private extension ArraySlice where Element == Token {
461464
range = range.lowerBound ..< endToken.range.upperBound
462465
case .keyword(.import):
463466
type = try .import(require(readExpressions(), as: "file path"))
464-
case .dot, .linebreak, .keyword, .infix, .lbrace, .rbrace, .rparen, .eof:
467+
case .dot, .linebreak, .keyword, .infix, .lbrace, .lbracket,
468+
.subscript, .rbrace, .rparen, .rbracket, .eof:
465469
self = start
466470
return nil
467471
}
468472
var expression = Expression(type: type, range: range)
469-
while case .dot = nextToken.type {
470-
removeFirst()
471-
let rhs = try require(readIdentifier(), as: "member name")
472-
expression = Expression(
473-
type: .member(expression, rhs),
474-
range: range.lowerBound ..< rhs.range.upperBound
475-
)
473+
loop: while true {
474+
switch nextToken.type {
475+
case .dot:
476+
removeFirst()
477+
let rhs = try require(readIdentifier(), as: "member name")
478+
expression = Expression(
479+
type: .member(expression, rhs),
480+
range: range.lowerBound ..< rhs.range.upperBound
481+
)
482+
case .subscript:
483+
removeFirst()
484+
let rhs = try require(readExpression(), as: "expression")
485+
try requireToken(.rbracket)
486+
expression = Expression(
487+
type: .subscript(expression, rhs),
488+
range: range.lowerBound ..< rhs.range.upperBound
489+
)
490+
default:
491+
break loop
492+
}
476493
}
477494
return expression
478495
}
@@ -665,9 +682,9 @@ private extension ArraySlice where Element == Token {
665682
default:
666683
return .expression(expression.type)
667684
}
668-
// TODO: should call be treated differently here?
669-
case .number, .linebreak, .keyword, .hexColor, .prefix,
670-
.string, .rbrace, .lparen, .call, .rparen, .eof:
685+
// TODO: should call and subscript be treated differently here?
686+
case .number, .linebreak, .keyword, .hexColor, .prefix, .string,
687+
.rbrace, .lparen, .call, .rparen, .lbracket, .rbracket, .subscript, .eof:
671688
return try .command(identifier, readExpressions())
672689
}
673690
}

ShapeScript/ProgramError.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public extension ProgramError {
7575
return baseURL
7676
}
7777
return error.shapeFileURL(relativeTo: url)
78-
case .unknownSymbol, .unknownMember, .unknownFont,
78+
case .unknownSymbol, .unknownMember, .invalidIndex, .unknownFont,
7979
.typeMismatch, .forwardReference, .unexpectedArgument,
8080
.missingArgument, .unusedValue, .assertionFailure,
8181
.fileNotFound, .fileTimedOut, .fileAccessRestricted,
@@ -94,8 +94,8 @@ public extension ProgramError {
9494
switch runtimeError.type {
9595
case let .importError(error, _, _):
9696
return error.underlyingError
97-
case .unknownSymbol, .unknownMember, .unknownFont, .typeMismatch,
98-
.forwardReference, .unexpectedArgument, .missingArgument,
97+
case .unknownSymbol, .unknownMember, .invalidIndex, .unknownFont,
98+
.typeMismatch, .forwardReference, .unexpectedArgument, .missingArgument,
9999
.unusedValue, .assertionFailure, .fileNotFound, .fileTimedOut,
100100
.fileAccessRestricted, .fileTypeMismatch, .fileParsingError,
101101
.circularImport:

ShapeScript/Types.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,11 +560,18 @@ extension Expression {
560560
for (type, expression) in zip(types, expressions) {
561561
expression.inferTypes(for: &params, in: context, with: type)
562562
}
563+
case let .list(type):
564+
for expression in expressions {
565+
expression.inferTypes(for: &params, in: context, with: type)
566+
}
563567
default:
564568
// TODO: other cases
565569
return
566570
}
567571
}
572+
case let .subscript(_, rhs):
573+
// TODO: lhs
574+
rhs.inferTypes(for: &params, in: context, with: .union([.number, .string]))
568575
case let .import(expression):
569576
expression.inferTypes(for: &params, in: context, with: .string)
570577
case let .infix(lhs, .step, rhs):
@@ -700,6 +707,16 @@ extension Expression {
700707
case let .member(expression, member):
701708
let type = try expression.staticType(in: context)
702709
return type.memberType(member.name) ?? .any
710+
case let .subscript(lhs, _):
711+
switch try lhs.staticType(in: context) {
712+
case let .list(type):
713+
return type
714+
case let .tuple(types):
715+
return .union(Set(types))
716+
default:
717+
// TODO: other cases
718+
return .any
719+
}
703720
case let .import(expression):
704721
var file: String?
705722
switch expression.type {

ShapeScriptTests/LexerTests.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,20 @@ class LexerTests: XCTestCase {
559559
XCTAssertEqual(try tokenize(input).map { $0.type }, tokens)
560560
}
561561

562+
// MARK: subscripting
563+
564+
func testSubscriptOnIdentifier() {
565+
let input = "a[\"x\"]"
566+
let tokens: [TokenType] = [.identifier("a"), .subscript, .string("x"), .rbracket, .eof]
567+
XCTAssertEqual(try tokenize(input).map { $0.type }, tokens)
568+
}
569+
570+
func testSubscriptOnIdentifier2() {
571+
let input = "a[1]"
572+
let tokens: [TokenType] = [.identifier("a"), .subscript, .number(1), .rbracket, .eof]
573+
XCTAssertEqual(try tokenize(input).map { $0.type }, tokens)
574+
}
575+
562576
// MARK: lineRange
563577

564578
func testLineRangeOfIndexAtStartOfInput() {

ShapeScriptTests/MemberTests.swift

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import XCTest
1212

1313
final class MemberTests: XCTestCase {
14+
// MARK: member access
15+
1416
func testTupleVectorLookup() {
1517
let program = "print (1 0).x"
1618
let delegate = TestDelegate()
@@ -566,4 +568,78 @@ final class MemberTests: XCTestCase {
566568
"""
567569
XCTAssertNoThrow(try evaluate(parse(program), delegate: nil))
568570
}
571+
572+
// MARK: subscripting
573+
574+
func testTupleVectorSubscripting() {
575+
let program = "print (1 0)[\"x\"]"
576+
let delegate = TestDelegate()
577+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
578+
XCTAssertEqual(delegate.log, [1.0])
579+
}
580+
581+
func testTupleVectorIndexing() {
582+
let program = "print (1 0)[0]"
583+
let delegate = TestDelegate()
584+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
585+
XCTAssertEqual(delegate.log, [1.0])
586+
}
587+
588+
func testOutOfBoundsTupleVectorSubscripting() {
589+
let program = "print (1 0)[\"z\"]"
590+
let delegate = TestDelegate()
591+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
592+
XCTAssertEqual(delegate.log, [0.0])
593+
}
594+
595+
func testNonExistentTupleVectorSubscripting() {
596+
let program = "print (1 0)[\"w\"]"
597+
XCTAssertThrowsError(try evaluate(parse(program), delegate: nil)) { error in
598+
let error = try? XCTUnwrap(error as? RuntimeError)
599+
guard case .unknownMember("w", of: "tuple", _)? = error?.type else {
600+
XCTFail()
601+
return
602+
}
603+
}
604+
}
605+
606+
func testOutOfBoundsTupleVectorIndexing() {
607+
let program = "print (1 0)[2]"
608+
XCTAssertThrowsError(try evaluate(parse(program), delegate: nil)) { error in
609+
let error = try? XCTUnwrap(error as? RuntimeError)
610+
guard case .invalidIndex(2, range: 0 ..< 2)? = error?.type else {
611+
XCTFail()
612+
return
613+
}
614+
}
615+
}
616+
617+
func testFunctionResultNestedTupleValueIndexing() {
618+
let program = """
619+
define a(b) {
620+
(b + 1 b + 2)
621+
(b + 3 b + 4)
622+
}
623+
define c a(4)
624+
print c[1][0]
625+
"""
626+
let delegate = TestDelegate()
627+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
628+
XCTAssertEqual(delegate.log, [7])
629+
}
630+
631+
func testFunctionMixedTupleValueLookupAndIndexing() {
632+
let program = """
633+
define a(b) {
634+
(b + 1 b + 2)
635+
(b + 3 b + 4)
636+
}
637+
define c a(4)
638+
print c.second[0]
639+
print c[1].first
640+
"""
641+
let delegate = TestDelegate()
642+
XCTAssertNoThrow(try evaluate(parse(program), delegate: delegate))
643+
XCTAssertEqual(delegate.log, [7, 7])
644+
}
569645
}

Viewer/iOS/SourceViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ extension TokenView.TokenType {
8383
extension TokenType {
8484
var tokenViewType: TokenView.TokenType {
8585
switch self {
86-
case .dot, .prefix, .infix, .lbrace, .rbrace, .lparen, .rparen, .call:
86+
case .dot, .prefix, .infix, .lbrace, .rbrace, .lparen, .rparen, .call, .lbracket, .rbracket, .subscript:
8787
return .operator
8888
case .identifier:
8989
return .identifier

0 commit comments

Comments
 (0)