Skip to content
This repository has been archived by the owner on Jul 3, 2022. It is now read-only.

Commit

Permalink
Merge pull request #45 from RoelAdriaans/feature/add-chapter-11-resol…
Browse files Browse the repository at this point in the history
…ving-binding

Feature/add chapter 11 resolving binding
  • Loading branch information
RoelAdriaans authored Oct 14, 2020
2 parents 468bb79 + 89331df commit 3833129
Show file tree
Hide file tree
Showing 11 changed files with 419 additions and 14 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.0.8] - 2020-10-14

### Added

- Completing chapter 11
- Create resolver
- Create function_type.py enum
- Create pytest fixtures to run a block of code, or an list of lines

### Changed

- Removed welcome statement during startup.
- Bumped version in `pyproject.toml`, this wasn't done in previous releases

## [0.0.7] - 2020-09-11

### Added
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "yaplox"
version = "0.0.3"
version = "0.0.8"
description = "YaPlox - Yet Another Python implementation for Lox"
authors = ["Roel Adriaans <[email protected]>"]
readme = "README.md"
Expand Down
21 changes: 20 additions & 1 deletion src/yaplox/environment.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
from __future__ import annotations

from typing import Any, Dict, Optional

from yaplox.token import Token
from yaplox.yaplox_runtime_error import YaploxRuntimeError


class Environment:
def __init__(self, enclosing: Optional["Environment"] = None):
def __init__(self, enclosing: Optional[Environment] = None):
self.values: Dict[str, Any] = dict()
self.enclosing = enclosing

def define(self, name: str, value: Any):
self.values[name] = value

def _ancestor(self, distance: int) -> Environment:
environment = self

for _ in range(distance):
environment = environment.enclosing # type: ignore

return environment

def get_at(self, distance: int, name: str) -> Any:
"""
Return a variable at a distance
"""
return self._ancestor(distance=distance).values.get(name)

def get(self, name: Token) -> Any:
try:
return self.values[name.lexeme]
Expand Down Expand Up @@ -39,3 +55,6 @@ def assign(self, name: Token, value: Any):
return

raise YaploxRuntimeError(name, f"Undefined variable '{name.lexeme}'.")

def assign_at(self, distance: int, name: Token, value: Any):
self._ancestor(distance).values[name.lexeme] = value
6 changes: 6 additions & 0 deletions src/yaplox/function_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import enum


class FunctionType(enum.Enum):
NONE = enum.auto()
FUNCTION = enum.auto()
20 changes: 17 additions & 3 deletions src/yaplox/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class Interpreter(ExprVisitor, StmtVisitor):
def __init__(self):
self.globals = Environment()
self.environment = self.globals
self.locals = dict()

self.globals.define("clock", Clock())

Expand All @@ -60,6 +61,9 @@ def interpret(self, statements: List[Stmt], on_error=None) -> Any:
def _execute(self, stmt: Stmt):
return stmt.accept(self)

def resolve(self, expr: Expr, depth: int):
self.locals[expr] = depth

@staticmethod
def _stringify(obj) -> str:
if obj is None:
Expand Down Expand Up @@ -208,12 +212,22 @@ def _evaluate(self, expr: Expr):
return expr.accept(self)

def visit_variable_expr(self, expr: "Variable") -> Any:
return self.environment.get(expr.name)
return self._look_up_variable(expr.name, expr)

def _look_up_variable(self, name: Token, expr: Expr) -> Any:
distance = self.locals.get(expr)
if distance is not None:
return self.environment.get_at(distance, name.lexeme)
else:
return self.globals.get(name)

def visit_assign_expr(self, expr: "Assign") -> Any:
value = self._evaluate(expr.value)

self.environment.assign(expr.name, value)
distance = self.locals.get(expr)
if distance:
self.environment.assign_at(distance, expr.name, value)
else:
self.globals.assign(expr.name, value)

return value

Expand Down
188 changes: 188 additions & 0 deletions src/yaplox/resolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
from collections import deque
from typing import Deque, List

from structlog import get_logger

from yaplox.expr import (
Assign,
Binary,
Call,
Expr,
ExprVisitor,
Grouping,
Literal,
Logical,
Unary,
Variable,
)
from yaplox.function_type import FunctionType
from yaplox.interpreter import Interpreter
from yaplox.stmt import (
Block,
Expression,
Function,
If,
Print,
Return,
Stmt,
StmtVisitor,
Var,
While,
)
from yaplox.token import Token

logger = get_logger()


class Resolver(ExprVisitor, StmtVisitor):
def __init__(self, interpreter: Interpreter, on_error=None):
self.interpreter = interpreter
self.scopes: Deque = deque()
self.on_error = on_error
self.current_function = FunctionType.NONE

def resolve(self, statements: List[Stmt]):
self._resolve_statements(statements)

def _resolve_statements(self, statements: List[Stmt]):
for statement in statements:
self._resolve_statement(statement)

def _resolve_statement(self, statement: Stmt):
statement.accept(self)

def _resolve_expression(self, expression: Expr):
expression.accept(self)

def _resolve_local(self, expr: Expr, name: Token):
for idx, scope in enumerate(reversed(self.scopes)):
if name.lexeme in scope:
self.interpreter.resolve(expr, idx)
return
# Not found. Assume it is global.

def _resolve_function(self, function: Function, type: FunctionType):
enclosing_function = self.current_function
self.current_function = type

self._begin_scope()
for param in function.params:
self._declare(param)
self._define(param)

self._resolve_statements(function.body)
self._end_scope()
self.current_function = enclosing_function

def _begin_scope(self):
self.scopes.append({})

def _end_scope(self):
self.scopes.pop()

def _declare(self, name: Token):
"""
Declare that a variable exists
Example is `var a;`
"""
if len(self.scopes) == 0:
return

# Look at the last scope
scope = self.scopes[-1]
if name.lexeme in scope:
self.on_error(name, "Already variable with this name in this scope.")

scope[name.lexeme] = False

def _define(self, name: Token):
"""
Declare that a variable is ready to use
Example: `a = 42;`
"""

if len(self.scopes) == 0:
return

scope = self.scopes[-1]
scope[name.lexeme] = True

def visit_assign_expr(self, expr: Assign):
self._resolve_expression(expr.value)
self._resolve_local(expr, expr.name)

def visit_binary_expr(self, expr: Binary):
self._resolve_expression(expr.left)
self._resolve_expression(expr.right)

def visit_call_expr(self, expr: Call):
self._resolve_expression(expr.callee)

for argument in expr.arguments:
self._resolve_expression(argument)

def visit_grouping_expr(self, expr: Grouping):
self._resolve_expression(expr.expression)

def visit_literal_expr(self, expr: Literal):
"""
Since a literal expression doesn't mention any variables and doesn't
contain any subexpressions, there is no work to do.
"""
return

def visit_logical_expr(self, expr: Logical):
self._resolve_expression(expr.left)
self._resolve_expression(expr.right)

def visit_unary_expr(self, expr: Unary):
self._resolve_expression(expr.right)

def visit_variable_expr(self, expr: Variable):
if len(self.scopes) != 0 and self.scopes[-1].get(expr.name.lexeme) is False:
self.on_error(
expr.name, "Cannot read local variable in its own initializer."
)
self._resolve_local(expr, expr.name)

def visit_block_stmt(self, stmt: Block):
self._begin_scope()
self._resolve_statements(stmt.statements)
self._end_scope()

def visit_expression_stmt(self, stmt: Expression):
self._resolve_expression(stmt.expression)

def visit_function_stmt(self, stmt: Function):
self._declare(stmt.name)
self._define(stmt.name)

self._resolve_function(stmt, FunctionType.FUNCTION)

def visit_if_stmt(self, stmt: If):
self._resolve_expression(stmt.condition)
self._resolve_statement(stmt.then_branch)
if stmt.else_branch:
self._resolve_statement(stmt.else_branch)

def visit_print_stmt(self, stmt: Print):
self._resolve_expression(stmt.expression)

def visit_return_stmt(self, stmt: Return):
if self.current_function == FunctionType.NONE:
self.on_error(stmt.keyword, "Can't return from top-level code.")

if stmt.value:
self._resolve_expression(stmt.value)

def visit_var_stmt(self, stmt: Var):
self._declare(stmt.name)

if stmt.initializer is not None:
self._resolve_expression(stmt.initializer)

self._define(stmt.name)

def visit_while_stmt(self, stmt: While):
self._resolve_expression(stmt.condition)
self._resolve_statement(stmt.body)
9 changes: 8 additions & 1 deletion src/yaplox/yaplox.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from yaplox.config import config # noqa: F401
from yaplox.interpreter import Interpreter
from yaplox.parser import Parser
from yaplox.resolver import Resolver
from yaplox.scanner import Scanner
from yaplox.token import Token
from yaplox.token_type import TokenType
Expand Down Expand Up @@ -36,6 +37,13 @@ def run(self, source: str):
logger.debug("Error after parsing")
return

resolver = Resolver(interpreter=self.interpreter, on_error=self.token_error)
resolver.resolve(statements)
# Stop if there was a resolution error.
if self.had_error:
logger.debug("Error after resolving")
return

self.interpreter.interpret(statements, on_error=self.runtime_error)

def error(self, line: int, message: str):
Expand Down Expand Up @@ -111,7 +119,6 @@ def main():
Run Yaplox from the console. Accepts one argument as a file that will be
executed, or no arguments to run in REPL mode.
"""
print("Welcome to yaplox.py")
if len(sys.argv) > 2:
print(f"Usage: {sys.argv[0]} [script]")
sys.exit(64)
Expand Down
28 changes: 27 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from typing import Optional
from typing import List, Optional

import pytest

from yaplox.token import Token
from yaplox.token_type import TokenType
from yaplox.yaplox import Yaplox


@pytest.fixture
Expand Down Expand Up @@ -40,3 +41,28 @@ def token_factory(
return token

return token_factory


@pytest.fixture
def run_code_lines(capsys):
def code_lines(lines: List[str]) -> capsys:

lines = "\n".join(lines)
Yaplox().run(lines)
captured = capsys.readouterr()

return captured

return code_lines


@pytest.fixture
def run_code_block(capsys):
def code_block(block: str) -> capsys:

Yaplox().run(block)
captured = capsys.readouterr()

return captured

return code_block
Loading

0 comments on commit 3833129

Please sign in to comment.