spinny-itty/spinny.js

441 lines
10 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)
function onRun() {
let cliArgs = parseCliArgs();
try { const fileobj = io.open(fs.resolve(cliArgs.filename), "r")
} catch (e) {
display.print(`Spinny: Error opening file or resolving path \`${cliArgs.filename}'.`);
io.error([22, "file open failed"]);
}
try { const file = fileobj.read();
} catch (e) {
display.print(`Spinny: Error reading file \`${cliArgs.filename}'.`);
io.error([23, "file read failed"]);
}
const fileSpincVersion = file.split("\n")[1].split("@");
let isSpincFile = false;
if (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"]);
}
if (!isSpincFile) {
// We must compile the file
const parser = new spinnyParser;
const compiled = parser.compile(file)
}
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'))
else
display.print(compiled)
}
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 = "";
let noMoreFlags = false;
let nextIsOutput = false
let output = "";
let letterFlag = false
let flag = false
for (let i=0;i<args.length;i++) {
let arg = args[i];
if (nextIsOutput) {
output = arg;
nextIsOutput = false;
}
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) {
if (arg === "compile" || arg === "c") {
compileOnly = true;
} else if (arg === "help" || arg === "h") {
showHelp();
quit();
} else if (arg === "version" || arg === "v") {
showVersion();
quit();
} else if (arg === "output" || arg === "o") {
nextIsOutput = true;
} else if (arg === "" && !letterFlag) {
noMoreFlags = true;
} else {
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;
}
return {
filename: filename,
compileOnly: compileOnly,
output: output
};
}
class spinnyParser {
INSIDE = {
FILE: 0,
INTER: 1,
LAZY: 2
}
compile(sourceFileContents) {
this.file = sourceFileContents;
this.index = 0;
this.debugStack = [];
return this.parseFile()
}
parseFile() {
this.debugStack.push(["File", this.index]);
let lines = [];
// A bunch of lines
while (this.index+1 < this.file.length) {
if (this.now() == "\n") {
this.next();
continue
}
lines.push(this.parseLine(INSIDE.FILE))
}
this.debugStack.pop();
return lines
}
now() { return this.file[this.index] }
next() { return this.file[this.index++] }
peek() { return this.file[this.index+1] }
parseLine(inside) {
this.debugStack.push(["Line", this.index])
let words = []
wordLoop:
while (1) {
switch (this.now()) {
case " ":
case "\t":
// advance past whitespace till
// next word
this.next();
continue
case "#":
this.parseComment();
continue
case "}":
if (inside === INSIDE.LAZY)
break wordLoop
break
case ">":
if (inside === INSIDE.INTER)
break wordLoop
break
case "\n":
if (inside === INSIDE.FILE
|| inside == 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(this.parseWord(inside));
}
this.debugStack.pop()
return words
}
parseComment() {
this.debugStack.push(["Comment", this.index])
commentLoop:
while (1) {
switch (this.now()) {
case "\n":
break commentLoop
case "\\":
if (this.peek() == "\n")
break commentLoop
default:
this.next();
}
}
this.debugStack.pop()
}
parseWord(inside) {
this.debugStack.push(["Word", this.index])
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] = this.parseLazy();
case "`":
word[CMPVER.KEYS.TYPE] = CMPVER.TYPE.LIT;
word[CMPVER.KEYS.PARTS] = this.parseLiteral();
default:
word[CMPVER.KEYS.TYPE] = CMPVER.TYPE.ID;
word[CMPVER.KEYS.PARTS] = this.parseIdentifier(inside);
}
this.debugStack.pop()
return word
}
parseLazy() {
this.debugStack.push(["Lazy", this.index])
this.next(); // Skip `{'
let lines = []
while (this.now() !== "}") {
lines.push(this.parseLine(INSIDE.LAZY))
}
this.debugStack.pop()
return lines
}
parseLiteral() {
this.debugStack.push(["Literal", this.index])
this.next(); // Skip `\`'
let parts = [""]
while (this.now() !== "\'") {
switch (this.now()) {
case "\\":
parts[parts.length-1]+=this.parseEscape();
break
case "<":
parts.push(
this.parseInterpolation()
);
parts.push("")
break
default:
parts[parts.length-1]+=this.now();
this.next();
break
}
}
this.debugStack.pop();
return parts
}
parseEscape() {
this.debugStack.push(["Literal", this.index])
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
}
this.debugStack.pop();
return out;
}
parseInterpolation() {
this.debugStack.push(["Interpolation", this.index])
this.next(); // skip `\<'
let line
while (this.now() !== ">") {
line = this.parseLine(INSIDE.INTER)
}
this.next(); // skip `>'
this.debugStack.pop()
return line
}
parseIdentifier(inside) {
this.debugStack.push(["Identifier", this.index])
let parts = [""]
idLoop:
while (1) {
switch (this.now()) {
case "\\":
parts[parts.length-1]+=this.parseEscape();
continue
case "<":
parts.push(
this.parseInterpolation()
);
parts.push("")
continue
case "}":
if (inside === INSIDE.LAZY)
break idLoop
break
case ">":
if (inside === INSIDE.INTER)
break idLoop
break
case "#":
break idLoop
}
parts[parts.length-1]+=this.now();
this.next();
}
this.debugStack.pop();
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:
onRun();
quit();