Skip to content

Commit

Permalink
Merge pull request #90 from sjbarag/array-and-aa-set
Browse files Browse the repository at this point in the history
feat(parse,interp): Implement setters for arrays and associative arrays
  • Loading branch information
sjbarag authored Dec 3, 2018
2 parents 8a5db90 + 063bf68 commit 74f7e5a
Show file tree
Hide file tree
Showing 24 changed files with 1,070 additions and 268 deletions.
9 changes: 9 additions & 0 deletions src/brsTypes/components/AssociativeArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,13 @@ export class AssociativeArray implements BrsValue, BrsComponent, BrsIterable {

return this.elements.get(index.value) || BrsInvalid.Instance;
}

set(index: BrsType, value: BrsType) {
if (index.kind !== ValueKind.String) {
throw new Error("Associative array indexes must be strings");
}

this.elements.set(index.value, value);
return BrsInvalid.Instance;
}
}
14 changes: 12 additions & 2 deletions src/brsTypes/components/BrsArray.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BrsValue, ValueKind, BrsBoolean } from "../BrsType";
import { BrsValue, ValueKind, BrsBoolean, BrsInvalid } from "../BrsType";
import { BrsType } from "..";
import { BrsComponent, BrsIterable } from "./BrsComponent";

Expand Down Expand Up @@ -45,6 +45,16 @@ export class BrsArray implements BrsValue, BrsComponent, BrsIterable {
throw new Error("Array indexes must be 32-bit integers");
}

return this.getElements()[index.getValue()];
return this.getElements()[index.getValue()] || BrsInvalid.Instance;
}

set(index: BrsType, value: BrsType) {
if (index.kind !== ValueKind.Int32) {
throw new Error("Array indexes must be 32-bit integers");
}

this.elements[index.getValue()] = value;

return BrsInvalid.Instance;
}
}
3 changes: 3 additions & 0 deletions src/brsTypes/components/BrsComponent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BrsType } from "..";
import { BrsInvalid } from "../BrsType";

export interface BrsComponent {

Expand All @@ -18,4 +19,6 @@ export interface BrsIterable {
* @returns the element at `index` if one exists, otherwise throws an Error.
*/
get(index: BrsType): BrsType;

set(index: BrsType, value: BrsType): BrsInvalid;
}
6 changes: 5 additions & 1 deletion src/interpreter/AstPrinter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as Expr from "../parser/Expression";
import * as Stmt from "../parser/Statement";
import { BrsType } from "../brsTypes";

/** Creates a pretty-printed representation of an expression to ease debugging. */
Expand Down Expand Up @@ -52,7 +53,10 @@ export class AstPrinter implements Expr.Visitor<string> {
visitM(e: Expr.M): string {
return JSON.stringify(e, undefined, 2);
}
visitSet(e: Expr.Set): string {
visitDottedSet(e: Stmt.DottedSet): string {
return JSON.stringify(e, undefined, 2);
}
visitIndexedSet(e: Stmt.IndexedSet): string {
return JSON.stringify(e, undefined, 2);
}
visitUnary(e: Expr.Unary): string {
Expand Down
10 changes: 5 additions & 5 deletions src/interpreter/Environment.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Token } from "../lexer";
import { Identifier } from "../lexer";
import { BrsType } from "../brsTypes";

/** The logical region from which a particular variable or function that defines where it may be accessed from. */
Expand Down Expand Up @@ -60,8 +60,8 @@ export class Environment {
this.function.delete(name.toLowerCase());
}

public get(name: Token): BrsType {
let lowercaseName = name.text!.toLowerCase();
public get(name: Identifier): BrsType {
let lowercaseName = name.text.toLowerCase();
let source = [this.function, this.module, this.global].find(scope =>
scope.has(lowercaseName)
);
Expand All @@ -73,8 +73,8 @@ export class Environment {
throw new NotFound(`Undefined variable '${name.text}'`);
}

public has(name: Token): boolean {
let lowercaseName = name.text!.toLowerCase();
public has(name: Identifier): boolean {
let lowercaseName = name.text.toLowerCase();
return [this.function, this.module, this.global].find(scope => scope.has(lowercaseName)) != null;
}

Expand Down
58 changes: 56 additions & 2 deletions src/interpreter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ export class Interpreter implements Expr.Visitor<BrsType>, Stmt.Visitor<BrsType>
let source = this.evaluate(expression.obj);
if (!isIterable(source)) {
throw BrsError.typeMismatch({
message: "Attemptin to retrieve property from non-iterable value",
message: "Attempting to retrieve property from non-iterable value",
line: expression.name.line,
left: source
});
Expand Down Expand Up @@ -776,7 +776,61 @@ export class Interpreter implements Expr.Visitor<BrsType>, Stmt.Visitor<BrsType>
visitM(expression: Expr.M) {
return BrsInvalid.Instance;
}
visitSet(expression: Expr.Set) {

visitDottedSet(statement: Stmt.DottedSet) {
let source = this.evaluate(statement.obj);
let value = this.evaluate(statement.value);

if (!isIterable(source)) {
throw BrsError.typeMismatch({
message: "Attempting to set property on non-iterable value",
line: statement.name.line,
left: source
});
return BrsInvalid.Instance;
}

try {
source.set(new BrsString(statement.name.text), value);
} catch (err) {
throw BrsError.make(err.message, statement.name.line);
}

return BrsInvalid.Instance;
}

visitIndexedSet(statement: Stmt.IndexedSet) {
let source = this.evaluate(statement.obj);

if (!isIterable(source)) {
BrsError.typeMismatch({
message: "Attempting to set property on non-iterable value",
line: statement.closingSquare.line,
left: source
});
return BrsInvalid.Instance;
}

let index = this.evaluate(statement.index);
if (!isBrsNumber(index) && !isBrsString(index)) {
BrsError.typeMismatch({
message: "Attempting to set property on iterable with illegal index type",
line: statement.closingSquare.line,
left: source,
right: index
});

return BrsInvalid.Instance;
}

let value = this.evaluate(statement.value);

try {
source.set(index, value);
} catch (err) {
throw BrsError.make(err.message, statement.closingSquare.line);
}

return BrsInvalid.Instance;
}

Expand Down
15 changes: 1 addition & 14 deletions src/parser/Expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export interface Visitor<T> {
visitAALiteral(expression: AALiteral): T;
visitLogical(expression: Logical): T;
visitM(expression: M): T;
visitSet(expression: Set): T;
visitUnary(expression: Unary): T;
visitVariable(expression: Variable): T;
}
Expand Down Expand Up @@ -160,18 +159,6 @@ export class M implements Expression {
}
}

export class Set implements Expression {
constructor(
readonly obj: Expression,
readonly name: Token,
readonly value: Expression
) {}

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

export class Unary implements Expression {
constructor(
readonly operator: Token,
Expand All @@ -185,7 +172,7 @@ export class Unary implements Expression {

export class Variable implements Expression {
constructor(
readonly name: Token
readonly name: Identifier
) {}

accept<R>(visitor: Visitor<R>): R {
Expand Down
33 changes: 30 additions & 3 deletions src/parser/Statement.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as Expr from "./Expression";
import { Token } from "../lexer";
import { Token, Identifier } from "../lexer";
import { BrsType } from "../brsTypes";

/** A set of reasons why a `Block` stopped executing. */
Expand All @@ -18,6 +18,8 @@ export interface Visitor<T> {
visitWhile(statement: While): BrsType;
visitNamedFunction(statement: Function): BrsType;
visitReturn(statement: Return): never;
visitDottedSet(statement: DottedSet): BrsType;
visitIndexedSet(statement: IndexedSet): BrsType;
}

/** A BrightScript statement */
Expand All @@ -32,7 +34,7 @@ export interface Statement {
}

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

accept<R>(visitor: Visitor<R>): BrsType {
return visitor.visitAssignment(this);
Expand Down Expand Up @@ -70,7 +72,7 @@ export class ExitWhile implements Statement {

export class Function implements Statement {
constructor(
readonly name: Token,
readonly name: Identifier,
readonly func: Expr.Function
) {}

Expand Down Expand Up @@ -171,4 +173,29 @@ export class While implements Statement {
accept<R>(visitor: Visitor<R>): BrsType {
return visitor.visitWhile(this);
}
}

export class DottedSet implements Statement {
constructor(
readonly obj: Expr.Expression,
readonly name: Identifier,
readonly value: Expr.Expression
) {}

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

export class IndexedSet implements Statement {
constructor(
readonly obj: Expr.Expression,
readonly index: Expr.Expression,
readonly value: Expr.Expression,
readonly closingSquare: Token
) {}

accept<R>(visitor: Visitor<R>): BrsType {
return visitor.visitIndexedSet(this);
}
}
27 changes: 21 additions & 6 deletions src/parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,15 @@ function functionDeclaration(isAnonymous: false): Stmt.Function;
function functionDeclaration(isAnonymous: boolean) {
let isSub = check(Lexeme.Sub);
let functionType = isSub ? "sub" : "function";
let name: Token;
let name: Identifier;
let returnType: ValueKind | undefined = ValueKind.Dynamic;

advance();

if (isAnonymous) {
consume(`Expected '(' after ${functionType}`, Lexeme.LeftParen);
} else {
name = consume(`Expected ${functionType} name after '${functionType}'`, Lexeme.Identifier);
name = consume(`Expected ${functionType} name after '${functionType}'`, Lexeme.Identifier) as Identifier;
consume(`Expected '(' after ${functionType} name`, Lexeme.LeftParen);
}

Expand Down Expand Up @@ -193,7 +193,7 @@ function assignment(...additionalterminators: Lexeme[]): Stmt.Assignment {
if (!check(...additionalterminators)) {
consume("Expected newline or ':' after assignment", Lexeme.Newline, Lexeme.Colon, Lexeme.Eof, ...additionalterminators);
}
return new Stmt.Assignment(name, value);
return new Stmt.Assignment(name as Identifier, value);
}

function statement(...additionalterminators: BlockTerminator[]): Statement {
Expand Down Expand Up @@ -383,7 +383,7 @@ function ifStatement(): Stmt.If {
return new Stmt.If(condition, thenBranch, elseIfBranches, elseBranch);
}

function expressionStatement(...additionalterminators: BlockTerminator[]): Stmt.Expression {
function expressionStatement(...additionalterminators: BlockTerminator[]): Stmt.Expression | Stmt.DottedSet | Stmt.IndexedSet {
let expressionStart = peek();
let expr = expression();

Expand All @@ -393,6 +393,21 @@ function expressionStatement(...additionalterminators: BlockTerminator[]): Stmt.

if (expr instanceof Expr.Call) {
return new Stmt.Expression(expr);
} else if (expr instanceof Expr.Binary) {
if (expr.left instanceof Expr.IndexedGet && expr.token.kind === Lexeme.Equal) {
return new Stmt.IndexedSet(
expr.left.obj,
expr.left.index,
expr.right,
expr.left.closingSquare
);
} else if (expr.left instanceof Expr.DottedGet && expr.token.kind === Lexeme.Equal) {
return new Stmt.DottedSet(
expr.left.obj,
expr.left.name,
expr.right
);
}
}

throw ParseError.make(expressionStart, "Expected statement or function call, but received an expression");
Expand Down Expand Up @@ -620,7 +635,7 @@ function primary(): Expression {
):
return new Expr.Literal(previous().literal!);
case match(Lexeme.Identifier):
return new Expr.Variable(previous());
return new Expr.Variable(previous() as Identifier);
case match(Lexeme.LeftParen):
let expr = expression();
consume("Unmatched '(' - expected ')' after expression", Lexeme.RightParen);
Expand Down Expand Up @@ -694,7 +709,7 @@ function primary(): Expression {

return new Expr.AALiteral(members);
case match(Lexeme.Pos, Lexeme.Tab):
let token = Object.assign(previous(), { kind: Lexeme.Identifier });
let token = Object.assign(previous(), { kind: Lexeme.Identifier }) as Identifier;
return new Expr.Variable(token);
default:
throw ParseError.make(peek(), `Found unexpected token '${peek().text}'`);
Expand Down
42 changes: 42 additions & 0 deletions test/brsTypes/Array.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,46 @@ describe("Array", () => {
);
});
});

describe("get", () => {
it("returns values from in-bounds indexes", () => {
let a = new BrsString("a");
let b = new BrsString("b");
let c = new BrsString("c");

let arr = new BrsArray([a, b, c]);

expect(arr.get(new Int32(0))).toBe(a);
expect(arr.get(new Int32(2))).toBe(c);
});

it("returns invalid for out-of-bounds indexes", () => {
let arr = new BrsArray([]);

expect(arr.get(new Int32(555))).toBe(BrsInvalid.Instance);
});
});

describe("set", () => {
it("sets values at in-bounds indexes", () => {
let a = new BrsString("a");
let b = new BrsString("b");
let c = new BrsString("c");

let arr = new BrsArray([a, b, c]);

arr.set(new Int32(0), new BrsString("replacement for a"));
arr.set(new Int32(2), new BrsString("replacement for c"));

expect(arr.get(new Int32(0))).toEqual(new BrsString("replacement for a"));
expect(arr.get(new Int32(2))).toEqual(new BrsString("replacement for c"));
});

it("sets values at out-of-bounds indexes", () => {
let arr = new BrsArray([]);

arr.set(new Int32(555), new BrsString("value set at index 555"));
expect(arr.get(new Int32(555))).toEqual(new BrsString("value set at index 555"));
});
});
});
Loading

0 comments on commit 74f7e5a

Please sign in to comment.