tabloid-fake-closure/main.js
2020-09-24 05:30:15 -04:00

668 lines
18 KiB
JavaScript

/* 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);
}