Skip to content

Commit

Permalink
Merge pull request #37 from quephird/implement_computed_properties
Browse files Browse the repository at this point in the history
Implement computed properties
  • Loading branch information
quephird committed Mar 31, 2024
2 parents 3ad7b51 + ec154d7 commit de0a24b
Show file tree
Hide file tree
Showing 12 changed files with 154 additions and 22 deletions.
3 changes: 2 additions & 1 deletion README.md
Expand Up @@ -28,6 +28,7 @@ So far, the following have been implemented in `slox`:
- List literals using square brackets
- Native functions for lists, `append()` and `deleteAt()`
- `break` and `continue` for flow control within loops
- Computed properties (inside classes, not at the top-level)

# Design

Expand All @@ -38,7 +39,7 @@ Most of the design of `slox` is fairly similar to the one in the book. There are
- resolving of variables from parsed code
- interpreting of resolved statements

However, unlike how they are implemented in the book, the REPL and file runner instantiate just the interpreter, passing in code to be executed; it is the interpreter that instantiates and runs each of the scanner, parser, resolver in succession, each feeding their results to the next. The interpreter also reads in a small standard library defined in a string; at this point, only a class declaration for a `List` class and some associated methods are defined in it.
However, unlike how they are implemented in the book, the REPL and file runner instantiate just the interpreter, passing in code to be executed; it is the interpreter that instantiates and runs the scanner, parser, resolver in succession, each feeding their results to the next. The interpreter also reads in a small standard library defined in a string; at this point, only a class declaration for a `List` class and some associated methods are defined in it.

There are a few other differences between this implementation and that in the book which are described below.

Expand Down
2 changes: 1 addition & 1 deletion slox/Expression.swift
Expand Up @@ -14,7 +14,7 @@ indirect enum Expression: Equatable {
case assignment(Token, Expression)
case logical(Expression, Token, Expression)
case call(Expression, Token, [Expression])
case lambda([Token], [Statement])
case lambda([Token]?, [Statement])
case get(Expression, Token)
case set(Expression, Token, Expression)
case this(Token)
Expand Down
11 changes: 9 additions & 2 deletions slox/Interpreter.swift
Expand Up @@ -532,7 +532,14 @@ class Interpreter {
throw RuntimeError.onlyInstancesHaveProperties
}

return try instance.get(propertyName: propertyNameToken.lexeme)
let property = try instance.get(propertyName: propertyNameToken.lexeme)

if case .userDefinedFunction(let userDefinedFunction) = property,
userDefinedFunction.isComputedProperty {
return try userDefinedFunction.call(interpreter: self, args: [])
}

return property
}

private func handleSetExpression(instanceExpr: ResolvedExpression,
Expand All @@ -552,7 +559,7 @@ class Interpreter {
return try environment.getValueAtDepth(name: thisToken.lexeme, depth: depth)
}

private func handleLambdaExpression(params: [Token], statements: [ResolvedStatement]) throws -> LoxValue {
private func handleLambdaExpression(params: [Token]?, statements: [ResolvedStatement]) throws -> LoxValue {
let environmentWhenDeclared = self.environment

let function = UserDefinedFunction(name: "lambda",
Expand Down
2 changes: 1 addition & 1 deletion slox/LoxClass.swift
Expand Up @@ -10,7 +10,7 @@ class LoxClass: LoxInstance, LoxCallable {
var superclass: LoxClass?
var arity: Int {
if let initializer = methods["init"] {
return initializer.params.count
return initializer.arity
}

return 0
Expand Down
12 changes: 6 additions & 6 deletions slox/Parser.swift
Expand Up @@ -138,12 +138,12 @@ struct Parser {
throw ParseError.missingFunctionName(currentToken)
}

if !currentTokenMatchesAny(types: [.leftParen]) {
throw ParseError.missingOpenParenForFunctionDeclaration(currentToken)
}
let parameters = try parseParameters()
if !currentTokenMatchesAny(types: [.rightParen]) {
throw ParseError.missingCloseParenAfterArguments(currentToken)
var parameters: [Token]? = nil
if currentTokenMatchesAny(types: [.leftParen]) {
parameters = try parseParameters()
if !currentTokenMatchesAny(types: [.rightParen]) {
throw ParseError.missingCloseParenAfterArguments(currentToken)
}
}

guard let functionBody = try parseBlock() else {
Expand Down
2 changes: 1 addition & 1 deletion slox/ResolvedExpression.swift
Expand Up @@ -14,7 +14,7 @@ indirect enum ResolvedExpression: Equatable {
case assignment(Token, ResolvedExpression, Int)
case logical(ResolvedExpression, Token, ResolvedExpression)
case call(ResolvedExpression, Token, [ResolvedExpression])
case lambda([Token], [ResolvedStatement])
case lambda([Token]?, [ResolvedStatement])
case get(ResolvedExpression, Token)
case set(ResolvedExpression, Token, ResolvedExpression)
case this(Token, Int)
Expand Down
12 changes: 8 additions & 4 deletions slox/Resolver.swift
Expand Up @@ -418,7 +418,7 @@ struct Resolver {
return .logical(resolvedLeftExpr, operToken, resolvedRightExpr)
}

mutating private func handleLambda(params: [Token],
mutating private func handleLambda(params: [Token]?,
statements: [Statement],
functionType: FunctionType) throws -> ResolvedExpression {
beginScope()
Expand All @@ -432,9 +432,13 @@ struct Resolver {
currentLoopType = previousLoopType
}

for param in params {
try declareVariable(name: param.lexeme)
defineVariable(name: param.lexeme)
if let params {
for param in params {
try declareVariable(name: param.lexeme)
defineVariable(name: param.lexeme)
}
} else if currentClassType == .none {
throw ResolverError.functionsMustHaveAParameterList
}

let resolvedStatements = try statements.map { statement in
Expand Down
3 changes: 3 additions & 0 deletions slox/ResolverError.swift
Expand Up @@ -20,6 +20,7 @@ enum ResolverError: CustomStringConvertible, Equatable, LocalizedError {
case cannotReferenceSuperWithoutSubclassing
case cannotBreakOutsideLoop
case cannotContinueOutsideLoop
case functionsMustHaveAParameterList

var description: String {
switch self {
Expand Down Expand Up @@ -47,6 +48,8 @@ enum ResolverError: CustomStringConvertible, Equatable, LocalizedError {
return "Can only `break` from inside a `while` or `for` loop"
case .cannotContinueOutsideLoop:
return "Can only `continue` from inside a `while` or `for` loop"
case .functionsMustHaveAParameterList:
return "Functions must have a parameter list"
}
}
}
21 changes: 15 additions & 6 deletions slox/UserDefinedFunction.swift
Expand Up @@ -7,19 +7,28 @@

struct UserDefinedFunction: LoxCallable, Equatable {
var name: String
var params: [Token]
var arity: Int {
return params.count
}
var params: [Token]?
var enclosingEnvironment: Environment
var body: [ResolvedStatement]
var isInitializer: Bool
var arity: Int {
if let params {
return params.count
} else {
return 0
}
}
var isComputedProperty: Bool {
return params == nil
}

func call(interpreter: Interpreter, args: [LoxValue]) throws -> LoxValue {
let newEnvironment = Environment(enclosingEnvironment: enclosingEnvironment)

for (i, arg) in args.enumerated() {
newEnvironment.define(name: params[i].lexeme, value: arg)
if let params {
for (i, arg) in args.enumerated() {
newEnvironment.define(name: params[i].lexeme, value: arg)
}
}

do {
Expand Down
21 changes: 21 additions & 0 deletions sloxTests/InterpreterTests.swift
Expand Up @@ -428,6 +428,27 @@ b.method()
XCTAssertEqual(actual, expected)
}

func testInterpretAccessingComputedPropertyOfClass() throws {
let input = """
class Circle {
init(radius) {
this.radius = radius;
}
area {
return 3.14159 * this.radius * this.radius;
}
}
var c = Circle(4);
c.area
"""

let interpreter = Interpreter()
let actual = try interpreter.interpretRepl(source: input)
let expected: LoxValue = .double(50.26544)
XCTAssertEqual(actual, expected)
}

func testInterpretAccessingElementOfList() throws {
let input = """
var foo = [1, 2, 3, 4, 5];
Expand Down
64 changes: 64 additions & 0 deletions sloxTests/ParserTests.swift
Expand Up @@ -1191,6 +1191,70 @@ final class ParserTests: XCTestCase {
XCTAssertEqual(actual, expected)
}

func testParseClassWithComputedProperty() throws {
// class Circle {
// area {
// return 3.14159 * this.radius * this.radius;
// }
// }
let tokens: [Token] = [
Token(type: .class, lexeme: "class", line: 1),
Token(type: .identifier, lexeme: "Circle", line: 1),
Token(type: .leftBrace, lexeme: "{", line: 1),

Token(type: .identifier, lexeme: "area", line: 2),
Token(type: .leftBrace, lexeme: "{", line: 2),

Token(type: .return, lexeme: "return", line: 3),
Token(type: .double, lexeme: "3.14159", line: 3),
Token(type: .star, lexeme: "*", line: 3),
Token(type: .this, lexeme: "this", line: 3),
Token(type: .dot, lexeme: ".", line: 3),
Token(type: .identifier, lexeme: "radius", line: 3),
Token(type: .star, lexeme: "*", line: 3),
Token(type: .this, lexeme: "this", line: 3),
Token(type: .dot, lexeme: ".", line: 3),
Token(type: .identifier, lexeme: "radius", line: 3),
Token(type: .semicolon, lexeme: ";", line: 3),

Token(type: .rightBrace, lexeme: "}", line: 4),

Token(type: .rightBrace, lexeme: "}", line: 5),
Token(type: .eof, lexeme: "", line: 5),
]

var parser = Parser(tokens: tokens)
let actual = try parser.parse()
let expected: [Statement] = [
.class(
Token(type: .identifier, lexeme: "Circle", line: 1),
nil,
[
.function(
Token(type: .identifier, lexeme: "area", line: 2),
.lambda(
nil,
[
.return(
Token(type: .return, lexeme: "return", line: 3),
.binary(
.binary(
.literal(.double(3.14159)),
Token(type: .star, lexeme: "*", line: 3),
.get(
.this(Token(type: .this, lexeme: "this", line: 3)),
Token(type: .identifier, lexeme: "radius", line: 3))),
Token(type: .star, lexeme: "*", line: 3),
.get(
.this(Token(type: .this, lexeme: "this", line: 3)),
Token(type: .identifier, lexeme: "radius", line: 3)))),
]))
],
[])
]
XCTAssertEqual(actual, expected)
}

func testParseListOfValues() throws {
// [1, "one", true]
let tokens: [Token] = [
Expand Down
23 changes: 23 additions & 0 deletions sloxTests/ResolverTests.swift
Expand Up @@ -94,6 +94,29 @@ final class ResolverTests: XCTestCase {
XCTAssertEqual(actual, expected)
}

func testResolveFunctionDeclarationWithoutParameterList() throws {
// fun answer {
// return 42;
// }
let statements: [Statement] = [
.function(
Token(type: .identifier, lexeme: "answer", line: 1),
.lambda(
nil,
[
.return(
Token(type: .return, lexeme: "return", line: 2),
.literal(.int(42)))
])),
]

var resolver = Resolver()
let expectedError = ResolverError.functionsMustHaveAParameterList
XCTAssertThrowsError(try resolver.resolve(statements: statements)) { actualError in
XCTAssertEqual(actualError as! ResolverError, expectedError)
}
}

func testResolveVariableExpressionInDeeplyNestedBlock() throws {
// var becca; {{{ becca = "awesome"; }}}
let statements: [Statement] = [
Expand Down

0 comments on commit de0a24b

Please sign in to comment.