summaryrefslogtreecommitdiff
path: root/static/js/lang.js
diff options
context:
space:
mode:
Diffstat (limited to 'static/js/lang.js')
-rw-r--r--static/js/lang.js637
1 files changed, 637 insertions, 0 deletions
diff --git a/static/js/lang.js b/static/js/lang.js
new file mode 100644
index 0000000..d5ed749
--- /dev/null
+++ b/static/js/lang.js
@@ -0,0 +1,637 @@
+/* Tabloid: the clickbait headline programming language */
+
+/* tokenizer */
+
+/**
+ * Reads in char or word chunks
+ */
+class Reader {
+ constructor(str, base = '') {
+ this.base = base;
+ this.i = 0;
+ this.str = str;
+ }
+ peek() {
+ return this.str[this.i];
+ }
+ next() {
+ return this.str[this.i++];
+ }
+ hasNext() {
+ return this.str[this.i] !== undefined;
+ }
+ backstep() {
+ this.i--;
+ }
+ readUntil(pred) {
+ let result = this.base.slice();
+ while (this.hasNext() && !pred(this.peek())) {
+ result += this.next();
+ }
+ return result;
+ }
+ dropWhitespace() {
+ this.readUntil(c => !!c.trim());
+ }
+ expect(tok) {
+ const next = this.next();
+ if (next !== tok) {
+ throw new Error(`Parsing error: expected ${tok}, got ${next}`);
+ }
+ }
+}
+
+/**
+ * Split into words for easier tokenization
+ * with keywords.
+ */
+class Wordifier {
+ constructor(str) {
+ this.reader = new Reader(prog);
+ this.tokens = [];
+ }
+ wordify() {
+ if (this.tokens.length) return this.tokens;
+
+ while (this.reader.hasNext()) {
+ const next = this.reader.next();
+ switch (next) {
+ case '(': {
+ this.tokens.push('(');
+ break;
+ }
+ case ')': {
+ this.tokens.push(')');
+ break;
+ }
+ case ',': {
+ this.tokens.push(',');
+ break;
+ }
+ case '"':
+ case "'": {
+ this.wordifyString(next);
+ break;
+ }
+ default: {
+ // read until WS
+ this.reader.backstep();
+ this.tokens.push(this.reader.readUntil(c => {
+ return !c.trim() || ['(', ')', ','].includes(c)
+ }));
+ }
+ }
+ this.reader.dropWhitespace();
+ }
+ return this.tokens.slice(1);
+ }
+ wordifyString(endChar) {
+ let acc = '';
+ acc += this.reader.readUntil(c => c == endChar);
+ while (acc.endsWith('\\') || !this.reader.hasNext()) {
+ acc += this.reader.readUntil(c => c != endChar);
+ }
+ this.reader.next(); // throw away closing char
+ this.tokens.push('"' + acc);
+ }
+}
+
+const T = {
+ LParen: Symbol('LParen'),
+ RParen: Symbol('RParen'),
+ Comma: Symbol('Comma'),
+ DiscoverHowTo: Symbol('DiscoverHowTo'),
+ With: Symbol('With'),
+ Of: Symbol('Of'),
+ WeSaid: Symbol('WeSaid'),
+ WhatIf: Symbol('WhatIf'),
+ LiesBang: Symbol('LiesBang'),
+ EndOfStory: Symbol('EndOfStory'),
+ ExpertsClaim: Symbol('ExpertsClaim'),
+ ToBe: Symbol('ToBe'),
+ YouWontWantToMiss: Symbol('YouWontWantToMiss'),
+ IsActually: Symbol('IsActually'),
+ And: Symbol('And'),
+ Or: Symbol('Or'),
+ Add: Symbol('Add'),
+ Subtract: Symbol('Subtract'),
+ Multiply: Symbol('Multiply'),
+ Divide: Symbol('Divide'),
+ Modulo: Symbol('Modulo'),
+ Beats: Symbol('Beats'), // >
+ SmallerThan: Symbol('SmallerThan'), // <
+ ShockingDevelopment: Symbol('ShockingDevelopment'),
+ PleaseLikeAndSubscribe: Symbol('PleaseLikeAndSubscribe'),
+
+ // not implemented yet
+ StayTuned: Symbol('StayTuned'),
+ Unexpectedly: Symbol('Unexpectedly'),
+ TotallyRight: Symbol('TotallyRight'),
+ CompletelyWrong: Symbol('CompletelyWrong'),
+}
+
+const BINARY_OPS = [
+ T.IsActually,
+ T.And,
+ T.Or,
+ T.Add,
+ T.Subtract,
+ T.Multiply,
+ T.Divide,
+ T.Modulo,
+ T.Beats,
+ T.SmallerThan,
+];
+
+function tokenize(prog) {
+ const reader = new Reader(new Wordifier(prog).wordify(), []);
+ const tokens = [];
+
+ while (reader.hasNext()) {
+ const next = reader.next();
+ switch (next) {
+ case 'DISCOVER': {
+ reader.expect('HOW');
+ reader.expect('TO');
+ tokens.push(T.DiscoverHowTo);
+ break;
+ }
+ case 'WITH': {
+ tokens.push(T.With);
+ break;
+ }
+ case 'OF': {
+ tokens.push(T.Of);
+ break;
+ }
+ case 'WE': {
+ reader.expect('SAID');
+ tokens.push(T.WeSaid);
+ break;
+ }
+ case 'WHAT': {
+ reader.expect('IF');
+ tokens.push(T.WhatIf);
+ break;
+ }
+ case 'LIES!': {
+ tokens.push(T.LiesBang);
+ break;
+ }
+ case 'END': {
+ reader.expect('OF');
+ reader.expect('STORY');
+ tokens.push(T.EndOfStory);
+ break;
+ }
+ case 'EXPERTS': {
+ reader.expect('CLAIM');
+ tokens.push(T.ExpertsClaim);
+ break;
+ }
+ case 'TO': {
+ reader.expect('BE');
+ tokens.push(T.ToBe);
+ break;
+ }
+ case 'YOU': {
+ reader.expect('WON\'T');
+ reader.expect('WANT');
+ reader.expect('TO');
+ reader.expect('MISS');
+ tokens.push(T.YouWontWantToMiss);
+ break;
+ }
+ case 'IS': {
+ reader.expect('ACTUALLY');
+ tokens.push(T.IsActually);
+ break;
+ }
+ case 'AND': {
+ tokens.push(T.And);
+ break;
+ }
+ case 'OR': {
+ tokens.push(T.Or);
+ break;
+ }
+ case 'ADD': {
+ tokens.push(T.Add);
+ break;
+ }
+ case 'SUBTRACT': {
+ tokens.push(T.Subtract);
+ break;
+ }
+ case 'MULTIPLY': {
+ tokens.push(T.Multiply);
+ break;
+ }
+ case 'DIVIDE': {
+ tokens.push(T.Divide);
+ break;
+ }
+ case 'MODULO': {
+ tokens.push(T.Modulo);
+ break;
+ }
+ case 'BEATS': {
+ tokens.push(T.Beats);
+ break;
+ }
+ case 'SMALLER': {
+ reader.expect('THAN');
+ tokens.push(T.SmallerThan);
+ break;
+ }
+ case 'SHOCKING': {
+ reader.expect('DEVELOPMENT');
+ tokens.push(T.ShockingDevelopment);
+ break;
+ }
+ case 'PLEASE': {
+ reader.expect('LIKE');
+ reader.expect('AND');
+ reader.expect('SUBSCRIBE');
+ tokens.push(T.PleaseLikeAndSubscribe);
+ break;
+ }
+ case 'STAY': {
+ reader.expect('TUNED');
+ tokens.push(T.StayTuned);
+ break;
+ }
+ case 'UNEXPECTEDLY': {
+ tokens.push(T.Unexpectedly);
+ break;
+ }
+ case 'TOTALLY': {
+ reader.expect('RIGHT');
+ tokens.push(T.TotallyRight);
+ break;
+ }
+ case 'COMPLETELY': {
+ reader.expect('WRONG');
+ tokens.push(T.CompletelyWrong);
+ break;
+ }
+ case '(': {
+ tokens.push(T.LParen);
+ break;
+ }
+ case ')': {
+ tokens.push(T.RParen);
+ break;
+ }
+ case ',': {
+ tokens.push(T.Comma);
+ break;
+ }
+ default: {
+ if (!isNaN(parseFloat(next))) {
+ // number literal
+ tokens.push(parseFloat(next));
+ } else {
+ // string or varname
+ tokens.push(next);
+ }
+ }
+ }
+ }
+ return tokens;
+}
+
+/* parser */
+
+const N = {
+ NumberLiteral: Symbol('NumberLiteral'),
+ StringLiteral: Symbol('StringLiteral'),
+ FnDecl: Symbol('FnDecl'),
+ FnCall: Symbol('FnCall'),
+ Ident: Symbol('Ident'),
+ Assignment: Symbol('Assignment'),
+ BinaryOp: Symbol('BinaryOp'),
+ IfExpr: Symbol('IfExpr'),
+ ExprGroup: Symbol('ExprGroup'),
+ ReturnExpr: Symbol('ReturnExpr'),
+ ProgEndExpr: Symbol('ProgEndExpr'),
+ PrintExpr: Symbol('PrintExpr'),
+}
+
+class Parser {
+ constructor(tokens) {
+ this.tokens = new Reader(tokens, []);
+ }
+ /**
+ * Atom
+ * Ident
+ * NumberLiteral
+ * StringLiteral
+ * FnCall
+ * FnDecl
+ * ExprGroup
+ *
+ * Expression:
+ * (begins with atom)
+ * BinaryOp
+ * Atom
+ * (begins with keyword)
+ * IfExpr
+ * Assignment
+ * ReturnExpr
+ * ProgEndExpr
+ * PrintExpr
+ *
+ */
+ parse() {
+ const nodes = [];
+ while (this.tokens.hasNext()) {
+ nodes.push(this.expr());
+ }
+ return nodes;
+ }
+ expectIdentString() {
+ const ident = this.tokens.next();
+ if (typeof ident === 'string' && !ident.startsWith('"')) {
+ return ident;
+ }
+ throw new Error(`Parsing error: expected identifier, got ${ident.toString()}`);
+ }
+ atom() {
+ const next = this.tokens.next();
+ if (typeof next === 'number') {
+ return {
+ type: N.NumberLiteral,
+ val: next,
+ }
+ } else if (typeof next === 'string') {
+ if (next.startsWith('"')) {
+ return {
+ type: N.StringLiteral,
+ val: next.substr(1),
+ }
+ }
+ const ident = {
+ type: N.Ident,
+ val: next,
+ }
+ if (this.tokens.peek() === T.Of) {
+ return this.fnCall(ident);
+ }
+ return ident;
+ } else if (next === T.DiscoverHowTo) {
+ // fn literal
+ const fnName = this.tokens.next();
+ if (this.tokens.peek(T.With)) {
+ this.tokens.next(); // with
+ // with args
+ const args = [this.expectIdentString()];
+ while (this.tokens.peek() === T.Comma) {
+ this.tokens.next(); // comma
+ args.push(this.expectIdentString());
+ }
+ return {
+ type: N.FnDecl,
+ name: fnName,
+ args: args,
+ body: this.expr(),
+ }
+ } else {
+ return {
+ type: N.FnDecl,
+ name: fnName,
+ args: [],
+ body: this.expr(),
+ }
+ }
+ } else if (next === T.WeSaid) {
+ // block
+ const exprs = [];
+ while (this.tokens.hasNext() && this.tokens.peek() !== T.EndOfStory) {
+ exprs.push(this.expr());
+ }
+ this.tokens.expect(T.EndOfStory);
+ return {
+ type: N.ExprGroup,
+ exprs: exprs,
+ };
+ }
+
+ throw new Error(`Parsing error: expected ident, literal, or block, got ${
+ next.toString()
+ } before ${this.tokens.peek().toString()}`);
+ }
+ expr() {
+ const next = this.tokens.next();
+ if (next === T.WhatIf) {
+ // if expr
+ const cond = this.expr();
+ const ifBody = this.expr();
+
+ let elseBody = null;
+ if (this.tokens.peek() == T.LiesBang) {
+ this.tokens.next(); // LiesBang
+ elseBody = this.expr();
+ }
+ return {
+ type: N.IfExpr,
+ cond: cond,
+ ifBody: ifBody,
+ elseBody: elseBody,
+ }
+ } else if (next === T.ExpertsClaim) {
+ // assignment
+ const name = this.expectIdentString();
+ this.tokens.expect(T.ToBe);
+ const val = this.expr();
+ return {
+ type: N.Assignment,
+ name,
+ val,
+ }
+ } else if (next === T.ShockingDevelopment) {
+ // return
+ return {
+ type: N.ReturnExpr,
+ val: this.expr(),
+ }
+ } else if (next === T.PleaseLikeAndSubscribe) {
+ // prog end
+ return {
+ type: N.ProgEndExpr,
+ }
+ } else if (next == T.YouWontWantToMiss) {
+ // print expr
+ return {
+ type: N.PrintExpr,
+ val: this.expr(),
+ }
+ }
+
+ this.tokens.backstep();
+ const atom = this.atom();
+ if (BINARY_OPS.includes(this.tokens.peek())) {
+ // infix binary ops
+ // TODO: support operator precedence
+ const left = atom;
+ const op = this.tokens.next();
+ const right = this.atom();
+ return {
+ type: N.BinaryOp,
+ op,
+ left,
+ right,
+ }
+ }
+
+ return atom;
+ }
+ fnCall(fnNode) {
+ this.tokens.expect(T.Of);
+ // TODO: support multiple arguments
+ const args = [this.expr()];
+ return {
+ type: N.FnCall,
+ fn: fnNode,
+ args: args,
+ }
+ }
+}
+
+/* executor (tree walk) */
+
+/**
+ * Abused (slightly) to easily return values upstack
+ */
+class ReturnError {
+ constructor(value) {
+ this.value = value;
+ }
+ unwrap() {
+ return this.value;
+ }
+}
+
+class Environment {
+ constructor(runtime) {
+ /**
+ * Runtime contains the following functions:
+ * - print(s)
+ */
+ this.runtime = runtime;
+ this.scopes = [{}]; // begin with global scope
+ }
+ run(nodes) {
+ let rv;
+ for (const node of nodes) {
+ rv = this.eval(node);
+ }
+ return rv;
+ }
+ eval(node) {
+ const scope = this.scopes[this.scopes.length - 1];
+
+ switch (node.type) {
+ case N.NumberLiteral: {
+ return node.val;
+ }
+ case N.StringLiteral: {
+ return node.val;
+ }
+ case N.FnDecl: {
+ scope[node.name] = node;
+ return node;
+ }
+ case N.FnCall: {
+ const fn = this.eval(node.fn);
+ const args = node.args.map(arg => this.eval(arg));
+
+ const calleeScope = {};
+ fn.args.forEach((argName, i) => {
+ calleeScope[argName] = args[i];
+ });
+
+ this.scopes.push(calleeScope);
+ let rv;
+ try {
+ this.eval(fn.body);
+ } catch (maybeReturnErr) {
+ if (maybeReturnErr instanceof ReturnError) {
+ rv = maybeReturnErr.unwrap();
+ } else {
+ // re-throw
+ throw maybeReturnErr;
+ }
+ }
+ this.scopes.pop();
+
+ return rv;
+ }
+ case N.Ident: {
+ let i = this.scopes.length - 1;
+ while (i >= 0) {
+ if (node.val in this.scopes[i]) {
+ return this.scopes[i][node.val];
+ }
+ i --;
+ }
+ throw new Error(`Runtime error: Undefined variable "${node.val}"`);
+ }
+ case N.Assignment: {
+ scope[node.name] = this.eval(node.val);
+ return scope[node.name];
+ }
+ case N.BinaryOp: {
+ const left = this.eval(node.left);
+ const right = this.eval(node.right);
+ switch (node.op) {
+ // TODO: other ops
+ case T.IsActually:
+ return left === right;
+ case T.Add:
+ return left + right;
+ case T.Subtract:
+ return left - right;
+ case T.Multiply:
+ return left * right;
+ default:
+ throw new Error(`Runtime error: Unknown binary op ${node.op.toString()}`);
+ }
+ }
+ case N.IfExpr: {
+ if (this.eval(node.cond)) {
+ return this.eval(node.ifBody);
+ }
+ if (node.elseBody != null) {
+ return this.eval(node.elseBody);
+ }
+ }
+ case N.ExprGroup: {
+ let rv = false; // TODO: make null value? make this illegal?
+ for (const expr of node.exprs) {
+ rv = this.eval(expr);
+ }
+ return rv;
+ }
+ case N.ReturnExpr: {
+ const rv = this.eval(node.val);
+ throw new ReturnError(rv);
+ }
+ case N.ProgEndExpr: {
+ // do nothing
+ break;
+ }
+ case N.PrintExpr: {
+ const val = this.eval(node.val);
+ Runtime.print(val);
+ return val;
+ }
+ default:
+ console.log(JSON.stringify(node, null, 2));
+ throw new Error(`Runtime error: Unknown AST Node of type ${
+ node.type.toString()
+ }:\n${JSON.stringify(node, null, 2)}`);
+ }
+ }
+}
+