This commit is contained in:
Elizabeth Hunt 2023-04-05 09:52:07 -06:00
parent 21a0f4df60
commit 5719d7e118
Signed by: simponic
GPG Key ID: 52B3774857EB24B1
2 changed files with 833 additions and 759 deletions

View File

@ -6,7 +6,7 @@
* Reads in char or word chunks
*/
class Reader {
constructor(str, base = '') {
constructor(str, base = "") {
this.base = base;
this.i = 0;
this.str = str;
@ -31,7 +31,7 @@ class Reader {
return result;
}
dropWhitespace() {
this.readUntil(c => !!c.trim());
this.readUntil((c) => !!c.trim());
}
expect(tok) {
const next = this.next();
@ -56,16 +56,16 @@ class Wordifier {
while (this.reader.hasNext()) {
const next = this.reader.next();
switch (next) {
case '(': {
this.tokens.push('(');
case "(": {
this.tokens.push("(");
break;
}
case ')': {
this.tokens.push(')');
case ")": {
this.tokens.push(")");
break;
}
case ',': {
this.tokens.push(',');
case ",": {
this.tokens.push(",");
break;
}
case '"':
@ -76,9 +76,11 @@ class Wordifier {
default: {
// read until WS
this.reader.backstep();
this.tokens.push(this.reader.readUntil(c => {
return !c.trim() || ['(', ')', ','].includes(c)
}));
this.tokens.push(
this.reader.readUntil((c) => {
return !c.trim() || ["(", ")", ","].includes(c);
})
);
}
}
this.reader.dropWhitespace();
@ -86,12 +88,12 @@ class Wordifier {
return this.tokens;
}
wordifyString(endChar) {
let acc = '';
acc += this.reader.readUntil(c => c == endChar);
while (acc.endsWith('\\') || !this.reader.hasNext()) {
let acc = "";
acc += this.reader.readUntil((c) => c == endChar);
while (acc.endsWith("\\") || !this.reader.hasNext()) {
acc = acc.substr(0, acc.length - 1);
this.reader.next(); // endChar
acc += endChar + this.reader.readUntil(c => c == endChar);
acc += endChar + this.reader.readUntil((c) => c == endChar);
}
this.reader.next(); // throw away closing char
this.tokens.push('"' + acc);
@ -99,35 +101,35 @@ class Wordifier {
}
const T = {
LParen: Symbol('LParen'),
RParen: Symbol('RParen'),
Comma: Symbol('Comma'),
DiscoverHowTo: Symbol('DiscoverHowTo'),
With: Symbol('With'),
Of: Symbol('Of'),
RumorHasIt: Symbol('RumorHasIt'),
WhatIf: Symbol('WhatIf'),
LiesBang: Symbol('LiesBang'),
EndOfStory: Symbol('EndOfStory'),
ExpertsClaim: Symbol('ExpertsClaim'),
ToBe: Symbol('ToBe'),
YouWontWantToMiss: Symbol('YouWontWantToMiss'),
LatestNewsOn: Symbol('LatestNewsOn'),
TotallyRight: Symbol('TotallyRight'),
CompletelyWrong: Symbol('CompletelyWrong'),
IsActually: Symbol('IsActually'),
And: Symbol('And'),
Or: Symbol('Or'),
Plus: Symbol('Plus'),
Minus: Symbol('Minus'),
Times: Symbol('Times'),
DividedBy: Symbol('DividedBy'),
Modulo: Symbol('Modulo'),
Beats: Symbol('Beats'), // >
SmallerThan: Symbol('SmallerThan'), // <
ShockingDevelopment: Symbol('ShockingDevelopment'),
PleaseLikeAndSubscribe: Symbol('PleaseLikeAndSubscribe'),
}
LParen: Symbol("LParen"),
RParen: Symbol("RParen"),
Comma: Symbol("Comma"),
DiscoverHowTo: Symbol("DiscoverHowTo"),
With: Symbol("With"),
Of: Symbol("Of"),
RumorHasIt: Symbol("RumorHasIt"),
WhatIf: Symbol("WhatIf"),
LiesBang: Symbol("LiesBang"),
EndOfStory: Symbol("EndOfStory"),
ExpertsClaim: Symbol("ExpertsClaim"),
ToBe: Symbol("ToBe"),
YouWontWantToMiss: Symbol("YouWontWantToMiss"),
LatestNewsOn: Symbol("LatestNewsOn"),
TotallyRight: Symbol("TotallyRight"),
CompletelyWrong: Symbol("CompletelyWrong"),
IsActually: Symbol("IsActually"),
And: Symbol("And"),
Or: Symbol("Or"),
Plus: Symbol("Plus"),
Minus: Symbol("Minus"),
Times: Symbol("Times"),
DividedBy: Symbol("DividedBy"),
Modulo: Symbol("Modulo"),
Beats: Symbol("Beats"), // >
SmallerThan: Symbol("SmallerThan"), // <
ShockingDevelopment: Symbol("ShockingDevelopment"),
PleaseLikeAndSubscribe: Symbol("PleaseLikeAndSubscribe"),
};
const BINARY_OPS = [
T.IsActually,
@ -149,139 +151,139 @@ function tokenize(prog) {
while (reader.hasNext()) {
const next = reader.next();
switch (next) {
case 'DISCOVER': {
reader.expect('HOW');
reader.expect('TO');
case "DISCOVER": {
reader.expect("HOW");
reader.expect("TO");
tokens.push(T.DiscoverHowTo);
break;
}
case 'WITH': {
case "WITH": {
tokens.push(T.With);
break;
}
case 'OF': {
case "OF": {
tokens.push(T.Of);
break;
}
case 'RUMOR': {
reader.expect('HAS');
reader.expect('IT');
case "RUMOR": {
reader.expect("HAS");
reader.expect("IT");
tokens.push(T.RumorHasIt);
break;
}
case 'WHAT': {
reader.expect('IF');
case "WHAT": {
reader.expect("IF");
tokens.push(T.WhatIf);
break;
}
case 'LIES!': {
case "LIES!": {
tokens.push(T.LiesBang);
break;
}
case 'END': {
reader.expect('OF');
reader.expect('STORY');
case "END": {
reader.expect("OF");
reader.expect("STORY");
tokens.push(T.EndOfStory);
break;
}
case 'EXPERTS': {
reader.expect('CLAIM');
case "EXPERTS": {
reader.expect("CLAIM");
tokens.push(T.ExpertsClaim);
break;
}
case 'TO': {
reader.expect('BE');
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');
case "YOU": {
reader.expect("WON'T");
reader.expect("WANT");
reader.expect("TO");
reader.expect("MISS");
tokens.push(T.YouWontWantToMiss);
break;
}
case 'LATEST': {
reader.expect('NEWS');
reader.expect('ON');
case "LATEST": {
reader.expect("NEWS");
reader.expect("ON");
tokens.push(T.LatestNewsOn);
break;
}
case 'IS': {
reader.expect('ACTUALLY');
case "IS": {
reader.expect("ACTUALLY");
tokens.push(T.IsActually);
break;
}
case 'AND': {
case "AND": {
tokens.push(T.And);
break;
}
case 'OR': {
case "OR": {
tokens.push(T.Or);
break;
}
case 'PLUS': {
case "PLUS": {
tokens.push(T.Plus);
break;
}
case 'MINUS': {
case "MINUS": {
tokens.push(T.Minus);
break;
}
case 'TIMES': {
case "TIMES": {
tokens.push(T.Times);
break;
}
case 'DIVIDED': {
reader.expect('BY');
case "DIVIDED": {
reader.expect("BY");
tokens.push(T.DividedBy);
break;
}
case 'MODULO': {
case "MODULO": {
tokens.push(T.Modulo);
break;
}
case 'BEATS': {
case "BEATS": {
tokens.push(T.Beats);
break;
}
case 'SMALLER': {
reader.expect('THAN');
case "SMALLER": {
reader.expect("THAN");
tokens.push(T.SmallerThan);
break;
}
case 'SHOCKING': {
reader.expect('DEVELOPMENT');
case "SHOCKING": {
reader.expect("DEVELOPMENT");
tokens.push(T.ShockingDevelopment);
break;
}
case 'PLEASE': {
reader.expect('LIKE');
reader.expect('AND');
reader.expect('SUBSCRIBE');
case "PLEASE": {
reader.expect("LIKE");
reader.expect("AND");
reader.expect("SUBSCRIBE");
tokens.push(T.PleaseLikeAndSubscribe);
break;
}
case 'TOTALLY': {
reader.expect('RIGHT');
case "TOTALLY": {
reader.expect("RIGHT");
tokens.push(T.TotallyRight);
break;
}
case 'COMPLETELY': {
reader.expect('WRONG');
case "COMPLETELY": {
reader.expect("WRONG");
tokens.push(T.CompletelyWrong);
break;
}
case '(': {
case "(": {
tokens.push(T.LParen);
break;
}
case ')': {
case ")": {
tokens.push(T.RParen);
break;
}
case ',': {
case ",": {
tokens.push(T.Comma);
break;
}
@ -302,21 +304,21 @@ function tokenize(prog) {
/* parser */
const N = {
NumberLiteral: Symbol('NumberLiteral'),
StringLiteral: Symbol('StringLiteral'),
BoolLiteral: Symbol('BoolLiteral'),
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'),
InputExpr: Symbol('InputExpr'),
}
NumberLiteral: Symbol("NumberLiteral"),
StringLiteral: Symbol("StringLiteral"),
BoolLiteral: Symbol("BoolLiteral"),
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"),
InputExpr: Symbol("InputExpr"),
};
class Parser {
constructor(tokens) {
@ -352,36 +354,40 @@ class Parser {
}
if (nodes[nodes.length - 1].type !== N.ProgEndExpr) {
throw new Error('Parsing error: A Tabloid program MUST end with PLEASE LIKE AND SUBSCRIBE');
throw new Error(
"Parsing error: A Tabloid program MUST end with PLEASE LIKE AND SUBSCRIBE"
);
}
return nodes;
}
expectIdentString() {
const ident = this.tokens.next();
if (typeof ident === 'string' && !ident.startsWith('"')) {
if (typeof ident === "string" && !ident.startsWith('"')) {
return ident;
}
throw new Error(`Parsing error: expected identifier, got ${ident.toString()}`);
throw new Error(
`Parsing error: expected identifier, got ${ident.toString()}`
);
}
atom() {
const next = this.tokens.next();
if (typeof next === 'number') {
if (typeof next === "number") {
return {
type: N.NumberLiteral,
val: next,
}
} else if (typeof next === 'string') {
};
} 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);
}
@ -390,12 +396,12 @@ class Parser {
return {
type: N.BoolLiteral,
val: true,
}
};
} else if (next === T.CompletelyWrong) {
return {
type: N.BoolLiteral,
val: false,
}
};
} else if (next === T.DiscoverHowTo) {
// fn literal
const fnName = this.tokens.next();
@ -412,14 +418,14 @@ class Parser {
name: fnName,
args: args,
body: this.expr(),
}
};
} else {
return {
type: N.FnDecl,
name: fnName,
args: [],
body: this.expr(),
}
};
}
} else if (next === T.RumorHasIt) {
// block
@ -445,9 +451,11 @@ class Parser {
};
}
throw new Error(`Parsing error: expected ident, literal, or block, got ${
next.toString()
} before ${this.tokens.peek().toString()}`);
throw new Error(
`Parsing error: expected ident, literal, or block, got ${next.toString()} before ${this.tokens
.peek()
.toString()}`
);
}
expr() {
const next = this.tokens.next();
@ -466,7 +474,7 @@ class Parser {
cond: cond,
ifBody: ifBody,
elseBody: elseBody,
}
};
} else if (next === T.ExpertsClaim) {
// assignment
const name = this.expectIdentString();
@ -476,30 +484,30 @@ class Parser {
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(),
}
};
} else if (next === T.LatestNewsOn) {
// input expr
return {
type: N.InputExpr,
val: this.expr(),
}
};
}
this.tokens.backstep();
@ -514,7 +522,7 @@ class Parser {
op,
left,
right,
}
};
}
return atom;
@ -526,11 +534,22 @@ class Parser {
this.tokens.next(); // comma
args.push(this.expr());
}
return {
// if (this.tokens.peek() === T.Of) {
// const x = this.fnCall(fnNode);
// return {
// type: N.FnCall,
// fn: x,
// args: args,
// };
// }
const x = {
type: N.FnCall,
fn: fnNode,
args: args,
}
};
// console.log("Top", x);
return x;
}
}
@ -565,6 +584,21 @@ class Environment {
}
return rv;
}
putClosureOnAllSubNodes(node, closure) {
if (Array.isArray(node)) {
for (let i = 0; i < node.length; ++i) {
node[i] = this.putClosureOnAllSubNodes(node[i], closure);
}
} else if (typeof node === "object") {
for (let key of Object.keys(node)) {
node[key] = this.putClosureOnAllSubNodes(node[key], closure);
}
node.closure = { ...closure };
}
return node;
}
eval(node) {
const scope = this.scopes[this.scopes.length - 1];
@ -574,13 +608,22 @@ class Environment {
case N.BoolLiteral:
return node.val;
case N.FnDecl: {
console.log("DECL", node, scope);
node.closure = { ...scope };
if (node.closure) {
node.closure = { ...node.closure, ...scope };
}
scope[node.name] = node;
return node;
}
case N.FnCall: {
const fn = this.eval(node.fn);
const args = node.args.map(arg => this.eval(arg));
console.log("fn - closure", node);
fn.body = this.putClosureOnAllSubNodes(fn.body, fn.closure);
console.log("fn", fn);
const args = node.args.map((arg) => this.eval(arg));
const calleeScope = {};
fn.args.forEach((argName, i) => {
calleeScope[argName] = args[i];
@ -608,12 +651,16 @@ class Environment {
if (node.val in this.scopes[i]) {
return this.scopes[i][node.val];
}
i --;
i--;
}
if (node.closure && node.closure.hasOwnProperty(node.val)) {
return node.closure[node.val];
}
throw new Error(`Runtime error: Undefined variable "${node.val}"`);
}
case N.Assignment: {
scope[node.name] = this.eval(node.val);
console.log("assn", node);
return scope[node.name];
}
case N.BinaryOp: {
@ -641,7 +688,9 @@ class Environment {
case T.SmallerThan:
return left < right;
default:
throw new Error(`Runtime error: Unknown binary op ${node.op.toString()}`);
throw new Error(
`Runtime error: Unknown binary op ${node.op.toString()}`
);
}
}
case N.IfExpr: {
@ -654,7 +703,9 @@ class Environment {
}
case N.ExprGroup: {
if (!node.exprs.length) {
throw new Error('Runtime error: Empty expression group with no expressions');
throw new Error(
"Runtime error: Empty expression group with no expressions"
);
}
let rv;
@ -675,9 +726,9 @@ class Environment {
let val = this.eval(node.val);
// shim for boolean to-string's
if (val === true) {
val = 'TOTALLY RIGHT';
val = "TOTALLY RIGHT";
} else if (val === false) {
val = 'COMPLETELY WRONG';
val = "COMPLETELY WRONG";
}
this.runtime.print(val);
return val;
@ -686,18 +737,21 @@ class Environment {
let val = this.eval(node.val);
// shim for boolean to-string's
if (val === true) {
val = 'TOTALLY RIGHT';
val = "TOTALLY RIGHT";
} else if (val === false) {
val = 'COMPLETELY WRONG';
val = "COMPLETELY WRONG";
}
return this.runtime.input(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)}`);
throw new Error(
`Runtime error: Unknown AST Node of type ${node.type.toString()}:\n${JSON.stringify(
node,
null,
2
)}`
);
}
}
}

View File

@ -1,34 +1,36 @@
const PROG_FACTORIAL = `YOU WON'T WANT TO MISS 'Hello, World!'
DISCOVER HOW TO factorial WITH n
const PROG_FIBONACCI = `
DISCOVER HOW TO link WITH a
RUMOR HAS IT
WHAT IF n IS ACTUALLY 0
DISCOVER HOW TO query WITH first
RUMOR HAS IT
WHAT IF first IS ACTUALLY 1
SHOCKING DEVELOPMENT a
LIES!
SHOCKING DEVELOPMENT 2
END OF STORY
SHOCKING DEVELOPMENT query
END OF STORY
EXPERTS CLAIM something TO BE link OF 3
YOU WON'T WANT TO MISS something OF 1
PLEASE LIKE AND SUBSCRIBE
`;
const PROG_FACTORIAL = `DISCOVER HOW TO link WITH a
RUMOR HAS IT
DISCOVER HOW TO query WITH first
RUMOR HAS IT
WHAT IF first IS ACTUALLY 1
SHOCKING DEVELOPMENT 1
LIES!
SHOCKING DEVELOPMENT
n TIMES factorial OF n MINUS 1
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 PROG_FIBONACCI = `DISCOVER HOW TO fibonacci WITH a, b, n
RUMOR HAS IT
WHAT IF n SMALLER THAN 1
SHOCKING DEVELOPMENT b
LIES! RUMOR HAS IT
YOU WON'T WANT TO MISS b
SHOCKING DEVELOPMENT
fibonacci OF b, a PLUS b, n MINUS 1
SHOCKING DEVELOPMENT 2
END OF STORY
SHOCKING DEVELOPMENT query
END OF STORY
EXPERTS CLAIM limit TO BE 10
YOU WON'T WANT TO MISS 'First 10 Fibonacci numbers'
EXPERTS CLAIM nothing TO BE fibonacci OF 0, 1, limit
YOU WON'T WANT TO MISS link OF 1 OF 2
PLEASE LIKE AND SUBSCRIBE`;
@ -44,65 +46,64 @@ const HEADLINES = [
`How To Lose Brain Fat With This Programming Language!`,
`Your Friends Will Be Jealous About This New Programming Language!`,
`You Can Earn Millions With This Programming Language!`,
`The Cure For Cancer Could Be Found With The Programming Language!`
`The Cure For Cancer Could Be Found With The Programming Language!`,
];
function randomHeadline() {
return HEADLINES[~~(Math.random() * HEADLINES.length)];
}
const {
Component,
} = window.Torus;
const { Component } = window.Torus;
class Editor extends Component {
init() {
this.prog = PROG_DEFAULT;
// script appends to it
this.output = '';
this.errors = '';
this.output = "";
this.errors = "";
this.handleRun = () => this.eval();
this.handleInput = evt => {
this.handleInput = (evt) => {
this.prog = evt.target.value;
this.render();
}
this.handleKeydown = evt => {
if (evt.key === 'Tab') {
};
this.handleKeydown = (evt) => {
if (evt.key === "Tab") {
evt.preventDefault();
const idx = evt.target.selectionStart;
if (idx !== null) {
const front = this.prog.substr(0, idx);
const back = this.prog.substr(idx);
this.prog = front + ' ' + back;
this.prog = front + " " + back;
this.render();
evt.target.setSelectionRange(idx + 4, idx + 4);
}
}
}
};
this.setFactorial = () => {
this.prog = PROG_FACTORIAL;
this.output = this.errors = '';
this.output = this.errors = "";
this.render();
}
this.setFibonacci= () => {
};
this.setFibonacci = () => {
this.prog = PROG_FIBONACCI;
this.output = this.errors = '';
this.output = this.errors = "";
this.render();
}
};
}
eval() {
this.output = '';
this.errors = '';
this.output = "";
this.errors = "";
try {
const tokens = tokenize(this.prog);
const nodes = new Parser(tokens).parse();
const env = new Environment({
print: s => {
this.output += s.toString().toUpperCase() + '!\n';
print: (s) => {
console.log(s);
this.output += s.toString().toUpperCase() + "!\n";
this.render();
},
input: s => {
input: (s) => {
return prompt(s);
},
});
@ -116,16 +117,23 @@ class Editor extends Component {
return jdom`<div class="editor fixed block">
<div class="controls">
<button class="block"
onclick=${this.setFibonacci}>Fibonacci <span class="desktop">sample</span></button>
onclick=${
this.setFibonacci
}>Fibonacci <span class="desktop">sample</span></button>
<button class="block"
onclick=${this.setFactorial}>Factorial <span class="desktop">sample</span></button>
onclick=${
this.setFactorial
}>Factorial <span class="desktop">sample</span></button>
<button class="accent block"
onclick=${this.handleRun}>Run<span class="desktop"> this</span>!</button>
onclick=${
this.handleRun
}>Run<span class="desktop"> this</span>!</button>
</div>
<div class="code">
<div class="filler">
${this.prog.split('\n')
.map(line => jdom`<p>${line.trim() ? line : '-'}</p>`)}
${this.prog
.split("\n")
.map((line) => jdom`<p>${line.trim() ? line : "-"}</p>`)}
</div>
<textarea class="editor-input" cols="30" rows="10"
value=${this.prog}
@ -134,14 +142,26 @@ class Editor extends Component {
</textarea>
</div>
<div class="output">
${this.output ? this.output
.split('\n')
.map(line => jdom`<code class="output-line">${line}</code>`)
: jdom`<code class="no-output">No output.</code>`}
${
this.output
? this.output
.split("\n")
.map(
(line) =>
jdom`<code class="output-line">${line}</code>`
)
: jdom`<code class="no-output">No output.</code>`
}
</div>
${this.errors ? jdom`<div class="errors">
${this.errors.split('\n').map(line => jdom`<code>${line}</code>`)}
</div>` : null}
${
this.errors
? jdom`<div class="errors">
${this.errors
.split("\n")
.map((line) => jdom`<code>${line}</code>`)}
</div>`
: null
}
</div>`;
}
}
@ -157,7 +177,7 @@ class App extends Component {
<nav>
<a href="https://github.com/thesephist/tabloid"
target="_blank" noopener noreferer>GitHub</a>
<a href="#" onclick=${evt => {
<a href="#" onclick=${(evt) => {
evt.preventDefault();
this.render();
}}>NEW headline!</a>
@ -176,7 +196,7 @@ class App extends Component {
headlines.
</p>
<p>
Here are <strike>a few things</strike>${' '}<strong>the Top Five
Here are <strike>a few things</strike>${" "}<strong>the Top Five
Most Popular Quirks and Features</strong> of the Tabloid
programming language <strong>(Number Four Will Shock You!)</strong>
</p>
@ -242,7 +262,7 @@ class App extends Component {
</p>
<p>
Before making Tabloid, I also created a more <strike>useful and
well-designed</strike>${' '}<strong>boring and unpopular</strong>
well-designed</strike>${" "}<strong>boring and unpopular</strong>
programming language, called <a href="https://dotink.co/"
target="_blank">Ink</a>.
</p>