547 lines
13 KiB
JavaScript
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"]);
|
|
}
|