summaryrefslogtreecommitdiff
path: root/main.js
diff options
context:
space:
mode:
authorLinus Lee <linus@thesephist.com>2020-09-24 05:45:33 -0400
committerLinus Lee <linus@thesephist.com>2020-09-24 05:45:33 -0400
commit030094045aa127e1e2a3800624c6a3e45c4c8c90 (patch)
treebea4c989bcb3e8312f765ccf4f9a1751c0b3cd6e /main.js
parentc79c0e4fec62e535732fff69fc216a0f31f95566 (diff)
downloadtabloid-fake-closure-030094045aa127e1e2a3800624c6a3e45c4c8c90.tar.gz
tabloid-fake-closure-030094045aa127e1e2a3800624c6a3e45c4c8c90.zip
Begin working on web app
Diffstat (limited to 'main.js')
-rw-r--r--main.js667
1 files changed, 0 insertions, 667 deletions
diff --git a/main.js b/main.js
deleted file mode 100644
index 67ef012..0000000
--- a/main.js
+++ /dev/null
@@ -1,667 +0,0 @@
-/* the clickbait headline programming language */
-
-const prog = `
-DISCOVER HOW TO factorial WITH n
-WE SAID
- WHAT IF n IS ACTUALLY 0
- WE SAID
- SHOCKING DEVELOPMENT 1
- END OF STORY
- LIES! WE SAID
- SHOCKING DEVELOPMENT n MULTIPLY factorial OF n SUBTRACT 1
- END OF STORY
-END OF STORY
-
-EXPERTS CLAIM result TO BE factorial OF 10
-YOU WON'T WANT TO MISS 'RESULT IS'
-YOU WON'T WANT TO MISS result
-
-PLEASE LIKE AND SUBSCRIBE
-`
-
-const Runtime = {
- print(s) {
- console.log(s.toString());
- }
-}
-
-/* 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() {
- 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 --;
- }
- console.log(this.scopes[this.scopes.length - 1]);
- 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)}`);
- }
- }
-}
-
-// main
-try {
- const tokens = tokenize(prog);
- const nodes = new Parser(tokens).parse();
- const env = new Environment();
- env.run(nodes);
-} catch (e) {
- console.error(e);
-}