Skip to content

Commit

Permalink
#52 this Keyword
Browse files Browse the repository at this point in the history
  • Loading branch information
dgp1130 committed Dec 25, 2016
1 parent 01b5554 commit 9685d2b
Show file tree
Hide file tree
Showing 13 changed files with 199 additions and 90 deletions.
6 changes: 3 additions & 3 deletions build/examples/car/car.ufm
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ result: {
};

make {
valid: make matches @filled;
valid: this matches @filled;
}

model {
valid: model matches @filled;
valid: this matches @filled;
}

year {
valid: year matches @year;
valid: this matches @year;
}
}
3 changes: 2 additions & 1 deletion src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,6 @@ export default {
MULTI_LINE: "m",
MATCH_LINE: "x"
},
ENDOFFILE: "EOF"
ENDOFFILE: "EOF",
THIS: "this"
};
23 changes: 16 additions & 7 deletions src/dependable.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
let initialized = Symbol("initialized");
let dependableSymbol = Symbol("dependable");
let dependents = Symbol("dependents");
let dependees = Symbol("dependees");
let expression = Symbol("expression");
const initialized = Symbol("initialized");
const dependableSymbol = Symbol("dependable");
const dependents = Symbol("dependents");
const dependees = Symbol("dependees");
const expression = Symbol("expression");
const addDependent = Symbol("addDependent");
const addDependee = Symbol("addDependee");

// Factory which creates a new class that extends the one given with the Dependable functionality mixed in
let Dependable = (Clazz = class { }) => class extends Clazz {
Expand All @@ -21,12 +23,12 @@ let Dependable = (Clazz = class { }) => class extends Clazz {
}

// Add the given object as dependent on this object
addDependent(dependent) {
[addDependent](dependent) {
this[dependents].push(dependent);
}

// Make this object dependent on the one given
addDependee(dependee) {
[addDependee](dependee) {
this[dependees].push(dependee);
}

Expand Down Expand Up @@ -62,4 +64,11 @@ Dependable.instanceof = function (obj) {
return obj[dependableSymbol] === true;
};

// Create dependency relationship between the two Dependables
// When the dependee changes, the dependent is updated
Dependable.addDependency = function (dependent, dependee) {
dependee[addDependent](dependent);
dependent[addDependee](dependee);
};

export default Dependable;
3 changes: 2 additions & 1 deletion src/lexer-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ export const createKeyword = (function () {
[constants.OPERATOR.THEN]: create,
[constants.OPERATOR.ELIF]: create,
[constants.OPERATOR.ELSE]: create,
[constants.OPERATOR.END]: create
[constants.OPERATOR.END]: create,
[constants.THIS]: create
};

// Return actual function returning a new token for the given keyword, or null if it is not a keyword
Expand Down
53 changes: 41 additions & 12 deletions src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import tokenizer from "./lexer.js";
import Identifier from "./identifier.js";
import { BlockVariable, ExpressionVariable } from "./variable.js";
import Tag from "./tag.js";
import Dependable from "./dependable.js";
import * as evaluator from "./evaluator.js";
import Scope from "./scope.js";

Expand All @@ -14,8 +15,6 @@ export default {
providedFile: false,

parse: function (input, spy) {
let self = this;

this.providedFile = true;

// Stack for holding tokens retrieved via lookahead()
Expand Down Expand Up @@ -117,7 +116,7 @@ export default {
let result;

// If testing expressions, declare variables then directly invoke and return expression()
if (self._testExpr) {
if (this._testExpr) {
while (currentToken.isUfmType()) decl();
result = expression(spy);
}
Expand Down Expand Up @@ -482,6 +481,8 @@ export default {
return identifier(owner);
} else if (currentToken.type === constants.TYPE.VARIABLE) { // Check for variable usage
return variable(owner);
} else if (currentToken.value === constants.THIS) { // Check for this keyword
return self(owner);
} else if (currentToken.isOperand()) { // Check for single-token operands
let operand = match();

Expand All @@ -506,8 +507,7 @@ export default {
if (!identifierObj) throw new UndeclaredError("Identifier " + identifierToken.value + " was not declared");

// Set the owner as dependent on the identifier
identifierObj.addDependent(owner);
owner.addDependee(identifierObj);
Dependable.addDependency(owner, identifierObj);

// Return the value of the identifier as the result
return () => identifierObj.value;
Expand All @@ -533,8 +533,7 @@ export default {
if (!tagObj) throw new NotImplementedError();

// Set the owner as dependent on the found tag
tagObj.addDependent(owner);
owner.addDependee(tagObj);
Dependable.addDependency(owner, tagObj);
});

return evaluator.dotTag(identifierToken, tag);
Expand All @@ -558,8 +557,7 @@ export default {
if (!(varObj instanceof ExpressionVariable)) throw new TypeError("Variable @" + varToken.value + " is a block, not an expression.");

// Set the owner as dependent on the variable
varObj.addDependent(owner);
owner.addDependee(varObj);
Dependable.addDependency(owner, varObj);
});

// Return the value of the variable as the result
Expand Down Expand Up @@ -587,13 +585,44 @@ export default {
if (!tagObj) throw new UndeclaredError("Tag @" + varToken.value + "." + tag.value + " was not declared");

// Set the owner as dependent on the found tag
tagObj.addDependent(owner);
owner.addDependee(tagObj);
Dependable.addDependency(owner, tagObj);
});

return evaluator.dotTag(varToken, tag);
}

// this
// Would love to call the function `this`, but JavaScript frowns on using keywords like that
function self(owner) {
match(); // this

// Owner must be Tag or ExpressionVariable
// Use owner of containing block and get its value
const context = owner.containingScope.owner;

// Verify that a valid context was found to bind `this` to
// This can occur if the developer uses `this` in the root scope
if (!context) {
throw new ParsingError("Could not find a valid containing scope to bind `this` keyword to.");
}

// Verify that context is not a BlockVariable, as `this` doesn't make sense there
if (context instanceof BlockVariable) {
throw new ParsingError("Can not bind `this` to a BlockVariable. It must be used under an Identifier block.");
}

// Verify assumption that context is an Identifier
if (!(context instanceof Identifier)) {
throw new AssertionError("Expected `this` owner to be an Identifier but it was actually "
+ context.toString());
}

// Set the owner as dependent on this value
Dependable.addDependency(owner, context);

return () => context.value;
}

// <object> -> { <keyValuePairs> }
function object(owner) {
let startToken = matchValue(constants.OPERATOR.LBRACE);
Expand Down Expand Up @@ -635,6 +664,6 @@ export default {
}

// Loaded all functions into the closure, parse the given input as a file
return file(spy); // Return for _testExpr === true case used in testing
return file.apply(this, [ spy ]); // Return for _testExpr === true case used in testing
}
};
4 changes: 3 additions & 1 deletion src/root.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ export default function () {
}

// Flatten the result
if (root.result) root.result.value = Token.flatten(root.result.value);
if (root.result) root.result = root.result.clone({
value: Token.flatten(root.result.value)
});

return root;
}
5 changes: 4 additions & 1 deletion src/stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,10 @@ export default class Stream {

// Save the given token to be returned by #extractResult().
returnToken(createToken) {
this.result = createToken(this.token);
this.result = createToken(this.token).clone({
line: this.line,
col: this.col
});
}

// Return the token saved by #returnToken() and reset the Stream.
Expand Down
25 changes: 21 additions & 4 deletions src/token.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,27 @@ export default class Token {
if (value === undefined) throw new Error("Undefined Value in new Token");
if (type === undefined) throw new Error("Undefined Type in new Token");

this.value = value;
this.type = type;
this.line = lineNumber;
this.col = lineIndex;
this._value = value;
this._type = type;
this._line = lineNumber;
this._col = lineIndex;
}

// Expose only getters to make Token immutable
get value() {
return this._value;
}

get type() {
return this._type;
}

get line() {
return this._line;
}

get col() {
return this._col;
}

// Clone this Token into a new Token, replacing the value or type as given
Expand Down
10 changes: 10 additions & 0 deletions test/helper/jasmineUtil.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Dependable from "../../src.es5/dependable.js";

beforeEach(function () {
// Spy on global JavaScript APIs to prevent their default behavior
if (globalObj.alert) spyOn(globalObj, "alert");
Expand Down Expand Up @@ -49,5 +51,13 @@ globalObj.jasmineUtil = {
// Return a promise which is automatically rejected with the error given
BrokenPromise: function (err) {
return new Promise((resolve, reject) => reject(err));
},

// Return a new dependable with the given expression
createDependable: function (expr = () => null) {
const clazz = new Dependable();
const dep = new clazz();
dep.initDependable(expr);
return dep;
}
};
57 changes: 17 additions & 40 deletions test/spec/dependable.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,54 +39,33 @@ describe("The Dependable module", function () {
});
});

let createDependable = function (expr = () => null) {
let clazz = Dependable();
let dep = new clazz();
dep.initDependable(expr);
return dep;
};

describe("exposes the \"instanceof\" member", function () {
it("as a static function", function () {
expect(Dependable.instanceof).toEqual(jasmine.any(Function))
});

it("which returns true for objects which mix in Dependable", function () {
expect(Dependable.instanceof(createDependable())).toBe(true);
expect(Dependable.instanceof(jasmineUtil.createDependable())).toBe(true);
});

it("which returns false for objects which do not mix in Dependable", function () {
expect(Dependable.instanceof({ })).toBe(false);
});
});

describe("exposes the \"addDependent\" member", function () {
it("as a function", function () {
expect(Dependable().prototype.addDependent).toEqual(jasmine.any(Function));
});

it("which adds the given object as a dependent of this Dependable", function () {
let first = createDependable();
let second = createDependable();

first.addDependent(second);

expect(first[Dependable._dependentsSymbol]).toEqual([ second ]);
});
});

describe("exposes the \"addDependee\" member", function () {
describe("exposes the \"addDependency\" static member", function () {
it("as a function", function () {
expect(Dependable().prototype.addDependee).toEqual(jasmine.any(Function));
expect(Dependable.addDependency).toEqual(jasmine.any(Function));
});

it("which sets this object as dependent on the given Dependable", function () {
let first = createDependable();
let second = createDependable();
it("which sets up a dependency between the two arguments", function () {
const dependent = jasmineUtil.createDependable();
const dependee = jasmineUtil.createDependable();

second.addDependee(first);
Dependable.addDependency(dependent, dependee);

expect(second[Dependable._dependeesSymbol]).toEqual([ first ]);
expect(dependent[Dependable._dependeesSymbol]).toEqual([ dependee ]);
expect(dependee[Dependable._dependentsSymbol]).toEqual([ dependent ]);
});
});

Expand All @@ -96,9 +75,9 @@ describe("The Dependable module", function () {
});

it("which triggers the \"update\" method on all this Dependable's dependents", function () {
let first = createDependable();
let second = createDependable();
let third = createDependable();
let first = jasmineUtil.createDependable();
let second = jasmineUtil.createDependable();
let third = jasmineUtil.createDependable();

spyOn(second, "update");
spyOn(third, "update");
Expand All @@ -117,15 +96,13 @@ describe("The Dependable module", function () {
});

it("which updates this Dependable's value with its expression and triggers it when its dependees are initialized", function () {
let dep = createDependable(() => "foo");
let dependee1 = createDependable(() => null);
let dependee2 = createDependable(() => null);
let dep = jasmineUtil.createDependable(() => "foo");
let dependee1 = jasmineUtil.createDependable(() => null);
let dependee2 = jasmineUtil.createDependable(() => null);

// dep is dependent on dependee1 and dependee2
dep.addDependee(dependee1);
dependee1.addDependent(dep);
dep.addDependee(dependee2);
dependee2.addDependent(dep);
Dependable.addDependency(dep, dependee1);
Dependable.addDependency(dep, dependee2);

spyOn(dep, "trigger");

Expand Down
4 changes: 4 additions & 0 deletions test/spec/lexer.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,10 @@ describe("The lexer module", function () {
it("end", function () {
assertKeyword("end", constants.OPERATOR.END);
});

it("this", function () {
assertKeyword("this", constants.THIS);
});
});

describe("operators like", function () {
Expand Down
Loading

0 comments on commit 9685d2b

Please sign in to comment.