spinny-itty/spinny.js

547 lines
13 KiB
JavaScript

////////////////////////////////////
// SPINNY : A Programming Language /
////////////////////////////////////
// This is the Spinny compiler and
// interpreter for itty.
// It accepts
// - Spinny source files (.spin)
// - Spinny compiled json files (.spic)
try {
async function onRun() {
let cliArgs = parseCliArgs();
let fileobj
let file
// File open:
try {
fileobj = io.open(fs.resolve(cliArgs.filename), "r")
} catch (e) {
display.print(`Spinny: Error opening file or resolving path \`${cliArgs.filename}'.`);
display.print(`${e.name}@${e.lineNumber}-${e.columnNumber}: ${e.message}`);
io.error([22, "file open failed"]);
}
try {
file = fileobj.read();
} catch (e) {
display.print(`Spinny: Error reading file \`${cliArgs.filename}'.`);
display.print(`${e.name}@${e.lineNumber}-${e.columnNumber}: ${e.message}`);
io.error([23, "file read failed"]);
}
// Spinc Checker:
let isSpincFile = false;
if (file.split("\n").length > 1) {
const fileSpincVersion = file.split("\n")[1].split("@");
if (fileSpincVersion.length > 2 &&
fileSpincVersion.slice(0,2).join("\n") === "SPINC\nspinny:itty") {
// This is a spinny source file
if (fileSpincVersion[2] !== "noversion") {
let niceversion = spileSpincVersion[2].replace(
/[\t %]/g , "")
display.print("Spinny: Incorrect or unrecognized compiled spinc file");
display.print(`Spinny: version \`${niceversion}'.`);
display.print("Spinny: This version of the spinny interpreter exclusively");
display.print("Spinny: accepts compiled spinc files of version");
display.print("Spinny: `noversion'.");
display.print("Spinny: Use `—version' or `—help' for more information.");
io.error([31, "invalid spinc version"]);
}
isSpincFile = true;
}
}
if (cliArgs.compileOnly && isSpincFile) {
display.print("Spinny: Cannot compile a spinc file.");
display.print("Spinny: The file is already compiled.");
io.error([32, "spinc already compiled"]);
}
let compiled
if (!isSpincFile) {
// We must compile the file
const parser = new spinnyParser({
parsestep:cliArgs.debugStep
});
let compiled;
try {
compiled = await parser.compile(file)
} catch (e) {
if (!(e instanceof spinnyCompileError))
throw e
let debug = e.debugStack
let last = e.debugStack.slice(-1)[0]
display.print("Spinny: Error compiling file...");
display.print(`Spinny: Unfinished ${last[0]} at ${last[1]}`);
io.error([45, "compilation failed"]);
}
display.print(JSON.stringify(compiled))
quit();
}
display.print("Spinny: Running and/or saving files is not currently supported.");
display.print("Spinny: Dumping contents instead.");
if (isSpincFile)
display.print(file.split("\n").slice(2).join('\n'))
}
function showHelp(){
display.print(
// . 1 . 2 . 3 . 4 . 5 . 6
// ^
"This is the Spinny compiler and interpreter for itty.\n" +
"It accepts...\n" +
" • Spinny source files (.spin)\n" +
" • Spinny compiled json files (.spinc)\n" +
"\n" +
"Syntax:\n" +
`\t${program.name} [flags...] [—] <filename>\n` +
"Where:\n" +
"\t[flags...] are any combination of the flags\n" +
"\t available in the `Flags' section\n" +
"\t below.\n" +
"\t[—] Is an emdash (—) or a double dash (--)\n" +
"\t to mark where the filename is supposed\n" +
"\t to go. (Note that any time you see an\n" +
"\t em dash in this help page, you may\n" +
"\t choose to use a double dash in its\n" +
"\t place if that is your preference.)\n" +
"\t<filename> is the `.spin' or `.spinc' file to\n" +
"\t compile/run\n" +
"Flags:\n" +
"\t—help Print this help page and exit.\n" +
"\t—version Print version and exit.\n" +
"\t—compile Compile the code to a .spinc file.\n" +
"\t The .spinc file must be specified\n" +
"\t with `—output'.\n" +
"\t—output <file> Specifies the output file for the\n" +
"\t `—compile' flag.\n"
);
};
function showVersion(){
display.print(
// . 1 . 2 . 3 . 4 . 5 . 6
// ^
"Spinny for itty\n" +
"Version 0.0.3\n" +
"Known spinc versions:\n" +
" • `noversion'\n"
)
};
function parseCliArgs() {
let compileOnly = false;
let filename = null;
let noMoreFlags = false;
let nextIsOutput = false
let output = "";
let letterFlag = false
let flag = false
let debugStep = false
for (let i=0;i<args.length;i++) {
let arg = args[i];
if (nextIsOutput) {
output = arg;
nextIsOutput = false;
continue;
}
if (!noMoreFlags) {
// Flags
flag = false;
letterFlag = false;
if (arg.slice(0,1) === "—") {
flag = true;
arg = arg.slice(1);
} else if (arg.slice(0,2) === "--") {
flag = true;
arg = arg.slice(2);
} else if (arg.slice(0,1) === "-") {
flag = true;
letterFlag = true;
arg = arg.slice(1);
}
if (flag) { switch (arg) {
case "c":
case "compile":
compileOnly = true;
break;
case "h":
case "help":
showHelp();
quit();
case "v":
case "version":
showVersion();
quit();
case "o":
case "output":
nextIsOutput = true;
break
case "debugstep":
debugStep = true;
case "":
if (!letterFlag) {
noMoreFlags = true;
break
}
default:
if (letterFlag){
display.print(`Spinny: Unrecognized flag \`-${arg}'.`);
} else {
display.print(`Spinny: Unrecognized flag \`${arg}'.`);
}
display.print("Spinny: For allowed flags, use `—help'.");
io.error([11, "unknown flag"]);
}; continue; }
}
filename = arg;
}
if (filename === null) {
display.print("Spinny: No filename provided :c");
display.print("Spinny: For help, use `—help'.");
io.error([13, "am i alone in this world?"]);
}
return {
filename: filename,
compileOnly: compileOnly,
output: output,
debugStep: debugStep
};
}
class spinnyCompileError extends Error {
constructor(message, debugStack) {
super(message);
this.name = "spinnyCompileError";
this.debugStack = debugStack;
}
}
class spinnyParser {
INSIDE = {
FILE: 0,
INTER: 1,
LAZY: 2
}
constructor(modifiers=null) {
this.mods = modifiers
}
async compile(sourceFileContents) {
this.file = sourceFileContents;
this.index = 0;
this.debugStack = [];
return await this.parseFile()
}
async parseFile() {
await this.tag("File")
let lines = [];
// A bunch of lines
while (this.index+1 < this.file.length) {
if (this.now() == "\n") {
this.next();
continue
}
lines.push(await this.parseLine(this.INSIDE.FILE))
}
await this.untag();
return lines
}
// Utility Functions
now() { return this.file[this.index] }
next() {
if (this.index++ === this.file.length)
throw new spinnyCompileError("Compilation failed at EOF", this.debugStack)
return this.file[this.index]
}
peek() { return this.file[this.index+1] }
async tag(tagtype) {
let stackupd = {
type: tagtype,
fpos: this.getFpos(this.index)
};
if (this.mods.parsestep) {
const regex = /\t/g
display.print(`Begin ${stackupd.type} @ ${stackupd.fpos[0]}:${stackupd.fpos[1]}`);
display.print(`${this.file.split('\n').slice(stackupd.fpos[0]-1)[0].replace(regex,"\u21a3")}`);
display.print(" " + "^".padStart(stackupd.fpos[1]));
if (await io.read() === "exit")
throw new spinnyCompileError("Compilation canceled by user.", this.debugStack)
}
this.debugStack.push(stackupd);
}
async untag() {
if (this.mods.parsestep) {
const regex = /\t/g
let stackupd = this.debugStack.slice(-1)[0];
let endFpos = this.getFpos(this.index)
display.print(`End ${stackupd.type} @ ${endFpos[0]}:${endFpos[1]}`);
display.print(`${this.file.split('\n').slice(endFpos[0]-1)[0].replace(regex,"\u21a3")}`);
display.print(" " + "^".padStart(endFpos[1]));
if (await io.read() === "exit")
throw new spinnyCompileError("Compilation canceled by user.", this.debugStack)
}
this.debugStack.pop();
}
getFpos(index) {
let upto = this.file.substring(0, index).split('\n');
return [upto.length, upto.slice(-1)[0].length + 1];
}
// Parsing Functions
async parseLine(inside) {
await this.tag("Line")
let words = []
wordLoop:
while (1) {
switch (this.now()) {
case " ":
case "\t":
// advance past whitespace till
// next word
this.next();
continue
case "#":
await this.parseComment();
continue
case "}":
if (inside === this.INSIDE.LAZY)
break wordLoop
break
case ">":
if (inside === this.INSIDE.INTER)
break wordLoop
break
case "\n":
if (inside === this.INSIDE.FILE
|| inside == this.INSIDE.LAZY)
break wordLoop
break
case "\\":
if (this.now() == "\\" && this.peek() == "\n") {
// Line continuation
// Go to next line
this.next(); // skip `\\'
this.next(); // skip `\n'
continue
}
break
}
words.push(await this.parseWord(inside));
}
await this.untag()
return words
}
async parseComment() {
await this.tag("Comment")
commentLoop:
while (1) {
switch (this.now()) {
case "\n":
break commentLoop
case "\\":
if (this.peek() == "\n")
break commentLoop
default:
this.next();
}
}
await this.untag()
}
async parseWord(inside) {
await this.tag("Word")
let bang = false;
let word = {}
word[CMPVER.KEYS.BANG] = false;
if (this.now() === "!") {
word[CMPVER.KEYS.BANG] = true;
this.next();
}
switch (this.now()) {
case "{":
word[CMPVER.KEYS.TYPE] = CMPVER.TYPE.LAZY;
word[CMPVER.KEYS.LINES] = await this.parseLazy();
break;
case "`":
word[CMPVER.KEYS.TYPE] = CMPVER.TYPE.LIT;
word[CMPVER.KEYS.PARTS] = await this.parseLiteral();
break;
default:
word[CMPVER.KEYS.TYPE] = CMPVER.TYPE.ID;
word[CMPVER.KEYS.PARTS] = await this.parseIdentifier(inside);
}
await this.untag()
return word
}
async parseLazy() {
await this.tag("Lazy")
this.next(); // Skip `{'
let lines = []
while (this.now() !== "}") {
lines.push(await this.parseLine(this.INSIDE.LAZY))
if (this.now() === "\n")
// Continue to next line like a file would
this.next();
}
this.next(); // Skip `}'
await this.untag()
return lines
}
async parseLiteral() {
await this.tag("Literal")
this.next(); // Skip `\`'
let parts = [""]
while (this.now() !== "\'") {
switch (this.now()) {
case "\\":
parts[parts.length-1]+=await this.parseEscape();
break
case "<":
parts.push(
await this.parseInterpolation()
);
parts.push("")
break
default:
parts[parts.length-1]+=this.now();
this.next();
break
}
}
this.next(); // Skip `\''
await this.untag();
return parts
}
async parseEscape() {
await this.tag("Escape")
this.next(); // skip `\\'
let out = "";
switch (this.now()) {
case "<":
this.next(); // skip `\<'
out = "<";
break
case "x":
this.next(); // skip `x'
out = String.fromCharCode( parseInt(
this.next()+this.next()
,16));
break
case "u":
this.next(); // skip `u'
out = String.fromCharCode( parseInt(
this.next()+this.next()+this.next()+this.next()
,16));
break
case "U":
this.next(); // skip `U'
out = String.fromCharCode( parseInt(
this.next()+this.next()+this.next()+this.next()+
this.next()+this.next()+this.next()+this.next()
,16));
break
case "s":
this.next(); // skip `s'
out = " ";
break
case "t":
this.next(); // skip `t'
out = "\t";
break
case "n":
this.next(); // skip `n'
out = "\n";
break
default:
// Just send the character:
out = this.now(); // record the thing after the `\\'
this.next(); // skip over it
break
}
await this.untag();
return out;
}
async parseInterpolation() {
await this.tag("Interpolation")
this.next(); // skip `\<'
let line
while (this.now() !== ">") {
line = await this.parseLine(this.INSIDE.INTER)
}
this.next(); // skip `>'
await this.untag()
return line
}
async parseIdentifier(inside) {
await this.tag("Identifier")
let parts = [""]
idLoop:
while (1) {
switch (this.now()) {
case "\\":
parts[parts.length-1]+=await this.parseEscape();
continue
case "<":
parts.push(
await this.parseInterpolation()
);
parts.push("")
continue
case "}":
if (inside === this.INSIDE.LAZY)
break idLoop
break
case ">":
if (inside === this.INSIDE.INTER)
break idLoop
break
case "#":
case " ":
case "\t":
case "\n":
break idLoop
}
parts[parts.length-1]+=this.now();
this.next();
}
await this.untag();
return parts
}
}
const SPIDNEY = {//spinny identifiers
"noversion": {
NAME: "noversion",
KEYS: {
TYPE: "t",
BANG: "b",
PARTS: "p",
LINES: "l"
},
TYPE: {
LIT: 0,
ID: 1,
LAZY: 2
}
}
}
const CMPVER = SPIDNEY["noversion"] //compileversion
// When the program starts:
await onRun();
quit();
} catch (e) {
display.print(`Spinny: Something has gone horribly wrong, commander.`);
display.print(`${e.name}@${e.lineNumber}-${e.columnNumber}: ${e.message}`);
io.error([9, "unknown error"]);
}