Skip to content

Commit

Permalink
Merge pull request #18 from sjbarag/add-global-scope-assignments
Browse files Browse the repository at this point in the history
Add global scope assignments
  • Loading branch information
sjbarag authored Jan 15, 2018
2 parents e1c7a3e + 6a67e98 commit 6cd68e7
Show file tree
Hide file tree
Showing 11 changed files with 307 additions and 23 deletions.
11 changes: 8 additions & 3 deletions src/Error.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
let foundError = false;

export function make(message: string, line: number, file?: string) {
foundError = true;
return runtime(message, line, file);
}

export function runtime(message: string, line: number, file?: string) {
let location = file ? `${file}: ${line}` : `line ${line}`;
let error = new Error(`[${location}] ${message}`);
if (process.env.NODE_ENV !== "test") {
console.error(`[${location}] ${message}`);
console.error(error);
}

foundError = true;
return error;
}

export function found() {
Expand Down
37 changes: 23 additions & 14 deletions src/grammar/BrightscriptGrammar.ebnf
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,29 @@
(* Start from the top, a brightscript script will have a number of "statements" before reaching the end of the file *)
program
: statementList? EOF
: declarationList? EOF
;
declarationList
: multiDeclaration+
;
(* brightscript supports multiple declarations on a line if they're separated bu ':' literals*)
multiDeclaration
: singleAssignment (":" singleDeclaration)* NEWLINE
;
singleAssignment
: assignment
| statementList
;
assignment
: IDENTIFIER assignmentOperator expression
;
assignmentOperator
: ("=" | "*=" | "/=" | "\=" | "+=" | "-=" | "<<=" | ">>=")
;
statementList
Expand Down Expand Up @@ -50,20 +72,7 @@ expressionStatement
;
expression
: assignmentExpression
;
assignmentExpression
: conditionalExpression
| assignment
;
assignment
: IDENTIFIER assignmentOperator expression
;
assignmentOperator
: ("=" | "*=" | "/=" | "\=" | "+=" | "-=" | "<<=" | ">>=")
;
(*
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { Executioner, isLong } from "./visitor/Executioner";
import { stringify } from "./Stringify";
import * as BrsError from "./Error";

const executioner = new Executioner();

export function execute(filename: string) {
fs.readFile(filename, "utf-8", (err, contents) => {
run(contents);
Expand Down Expand Up @@ -47,7 +49,6 @@ function run(contents: string) {
return;
}

const executioner = new Executioner();
if (!statements) { return; }

return executioner.exec(statements);
Expand Down
9 changes: 9 additions & 0 deletions src/parser/Statement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as Expr from "./Expression";
import { Token } from "../Token";

export interface Visitor<T> {
visitAssignment(statement: Assignment): T;
visitExpression(statement: Expression): T;
visitPrint(statement: Print): T;
}
Expand All @@ -10,6 +11,14 @@ export interface Statement {
accept <R> (visitor: Visitor<R>): R;
}

export class Assignment implements Statement {
constructor(readonly name: Token, readonly value: Expr.Expression) {}

accept<R>(visitor: Visitor<R>): R {
return visitor.visitAssignment(this);
}
}

export class Block implements Statement {
constructor(readonly statements: ReadonlyArray<Statement>) {}

Expand Down
44 changes: 42 additions & 2 deletions src/parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ export function parse(toParse: ReadonlyArray<Token>) {
current = 0;
tokens = toParse;

let statements = [];
let statements: Statement[] = [];

try {
while (!isAtEnd()) {
statements.push(statement());
let dec = declaration();
if (dec) {
statements.push(dec);
}
}

return statements;
Expand All @@ -26,6 +29,32 @@ export function parse(toParse: ReadonlyArray<Token>) {
}
}

function declaration(): Statement | undefined {
try {
// BrightScript is like python, in that variables can be declared without a `var`,
// `let`, (...) keyword. As such, we must check the token *after* an identifier to figure
// out what to do with it.
if (check(Lexeme.Identifier) && checkNext(Lexeme.Equal)) {
return assignment();
}

return statement();
} catch (error) {
synchronize();
return;
}
}

function assignment(): Statement {
let name = advance();
consume("Expected '=' after idenfifier", Lexeme.Equal);
// TODO: support +=, -=, >>=, etc.

let value = expression();
consume("Expected newline or ':' after assignment", Lexeme.Newline, Lexeme.Colon, Lexeme.Eof);
return new Stmt.Assignment(name, value);
}

function statement(): Statement {
if (match(Lexeme.Print)) {
return printStatement();
Expand Down Expand Up @@ -145,6 +174,8 @@ function primary(): Expression {
let p = previous();
let lit = new Expr.Literal(p.literal);
return lit;
case match(Lexeme.Identifier):
return new Expr.Variable(previous());
case match(Lexeme.LeftParen):
let expr = expression();
consume("Unmatched '(' - expected ')' after expression", Lexeme.RightParen);
Expand Down Expand Up @@ -186,10 +217,19 @@ function check(lexeme: Lexeme) {
return peek().kind === lexeme;
}

function checkNext(lexeme: Lexeme) {
return peekNext().kind === lexeme;
}

function isAtEnd() {
return peek().kind === Lexeme.Eof;
}

function peekNext() {
if (isAtEnd()) { return peek(); }
return tokens[current + 1];
}

function peek() {
return tokens[current];
}
Expand Down
19 changes: 19 additions & 0 deletions src/visitor/Environment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Token, Literal as TokenLiteral } from "../Token";
import { Lexeme } from "../Lexeme";
import * as BrsError from "../Error";

export default class Environment {
private values = new Map<string, TokenLiteral>();

public define(name: string, value: TokenLiteral): void {
this.values.set(name, value);
}

public get(name: Token): TokenLiteral {
if (this.values.has(name.text!)) {
return this.values.get(name.text!);
}

throw BrsError.runtime(`Undefined variable ${name.text}`, name.line);
}
}
16 changes: 13 additions & 3 deletions src/visitor/Executioner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { Lexeme } from "../Lexeme";
import { stringify } from "../Stringify";
import * as BrsError from "../Error";

import Environment from "./Environment";

export function isLong(arg: TokenLiteral): arg is Long {
return Long.isLong(arg);
}
Expand All @@ -20,10 +22,16 @@ function isString(arg: TokenLiteral): arg is string {
}

export class Executioner implements Expr.Visitor<TokenLiteral>, Stmt.Visitor<TokenLiteral> {
private environment = new Environment();

exec(statements: Stmt.Statement[]) {
return statements.map((statement) => this.execute(statement));
}

visitAssign(statement: Expr.Assign): TokenLiteral {
return undefined;
}

visitExpression(statement: Stmt.Expression): TokenLiteral {
return this.evaluate(statement.expression);
}
Expand All @@ -34,8 +42,10 @@ export class Executioner implements Expr.Visitor<TokenLiteral>, Stmt.Visitor<Tok
return;
}

visitAssign(expression: Expr.Assign) {
return undefined;
visitAssignment(statement: Stmt.Assignment): undefined {
let value = this.evaluate(statement.value);
this.environment.define(statement.name.text!, value);
return;
}

visitBinary(expression: Expr.Binary) {
Expand Down Expand Up @@ -385,7 +395,7 @@ export class Executioner implements Expr.Visitor<TokenLiteral>, Stmt.Visitor<Tok
}

visitVariable(expression: Expr.Variable) {
return undefined;
return this.environment.get(expression.name);
}

evaluate(expression: Expr.Expression): TokenLiteral {
Expand Down
52 changes: 52 additions & 0 deletions test/executioner/Variables.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const BrsError = require("../../lib/Error");
const Expr = require("../../lib/parser/Expression");
const Stmt = require("../../lib/parser/Statement");
const { identifier, token } = require("../parser/ParserTests");
const { Lexeme } = require("../../lib/Lexeme");
const { Executioner } = require("../../lib/visitor/Executioner");

let executioner;

describe("executioner", () => {
beforeEach(() => {
BrsError.reset();
executioner = new Executioner();
});

it("returns 'invalid' for assignments", () => {
let ast = new Stmt.Assignment(
identifier("foo"),
new Expr.Literal(5)
);

let result = executioner.exec([ast]);
expect(result).toEqual([undefined]);
});

it("stores assigned values in global scope", () => {
let ast = new Stmt.Assignment(
identifier("bar"),
new Expr.Literal(6)
);
executioner.exec([ast]);
expect(
executioner.environment.get(
identifier("bar")
)
).toBe(6);
});

it("retrieves variables from global scope", () => {
let assign = new Stmt.Assignment(
identifier("baz"),
new Expr.Literal(7)
);
let retrieve = new Stmt.Expression(
new Expr.Variable(
identifier("baz")
)
);
let results = executioner.exec([ assign, retrieve ]);
expect(results).toEqual([undefined, 7]);
});
});
13 changes: 13 additions & 0 deletions test/parser/ParserTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,18 @@ exports.token = function(kind, literal) {
};
}

/**
* Creates an Identifier token with the given `text`.
* @param {string} text
* @returns {object} a token with the provided `text`.
*/
exports.identifier = function(text) {
return {
kind: Lexeme.Identifier,
text: text,
line: 1
};
}

/** An end-of-file token. */
exports.EOF = exports.token(Lexeme.Eof);
Loading

0 comments on commit 6cd68e7

Please sign in to comment.