spinny-itty/spinny.js
2025-12-31 15:27:41 -06:00

407 lines
9.4 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) === ["SPINC","spinny: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.1\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];a
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,
compile_only: compileOnly,
output: output
};
}
class spinnyParser {
compile(sourceFileContents) {
this.file = sourceFileContents;
this.index = 0;
this.debugStack = [];
return parseFile()
}
parseFile() {
this.debugStack.push(["File", this.index]);
let lines = [];
// A bunch of lines
while (this.index+1 < this.file.length) {
if (now() == "\n") {
next();
continue
}
lines.push(parseLine("F"))
}
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 (now()) {
case " ":
case "\t":
// advance past whitespace till
// next word
next();
continue
case "#":
parseComment();
continue
case "}":
if (inside === "L")
break wordLoop
break
case ">":
if (inside === "I")
break wordLoop
break
case "\n":
if (inside === "F")
break wordLoop
break
case "\\":
if (now() == "\\" && peek() == "\n") {
// Line continuation
// Go to next line
next(); // skip `\\'
next(); // skip `\n'
continue
}
break
}
words.push(parseWord(inside));
}
this.debugStack.pop()
return words
}
parseComment() {
this.debugStack.push(["Comment", this.index])
commentLoop:
while (1) {
switch (now()) {
case "\n":
break commentLoop
case "\\":
if (peek() == "\n")
break commentLoop
default:
next();
}
}
this.debugStack.pop()
}
parseWord(inside) {
this.debugStack.push(["Word", this.index])
switch (now()) {
case "{":
return {type:"lazy",lines:parseLazy()}
case "`":
return {type:"literal",parts:parseLiteral()}
default:
return {type:"identifier",parts:parseIdentifier(inside)}
}
this.debugStack.pop()
}
parseLazy() {
this.debugStack.push(["Lazy", this.index])
next(); // Skip `{'
let lines = []
while (now() !== "}") {
lines.push(parseLine("L"))
}
this.debugStack.pop()
return lines
}
parseLiteral() {
this.debugStack.push(["Literal", this.index])
next(); // Skip `\`'
let parts = []
while (now() !== "\'") {
switch (now()) {
case "\\":
parts[parts.length-1]+=parseEscape();
break
case "<":
parts.push(
parseInterpolation()
);
parts.push("")
break
default:
parts[parts.length-1]+=now();
next();
break
}
}
this.debugStack.pop();
return parts
}
parseEscape() {
this.debugStack.push(["Literal", this.index])
next(); // skip `\\'
let out = "";
switch (now()) {
case "<":
next(); // skip `\<'
out = "<";
break
case "x":
next(); // skip `x'
out = String.fromCharCode(next()+next());
break
case "u":
next(); // skip `u'
out = String.fromCharCode(
next()+next()+next()+next()
);
break
case "U":
next(); // skip `U'
out = String.fromCharCode(
next()+next()+next()+next()+
next()+next()+next()+next()
);
break
case "s":
next(); // skip `s'
out = " ";
break
case "t":
next(); // skip `t'
out = "\t";
break
case "n":
next(); // skip `n'
out = "\n";
break
default:
// Just send the character:
out = now(); // record the thing after the `\\'
next(); // skip over it
break
}
this.debugStack.pop();
return out;
}
parseInterpolation() {
this.debugStack.push(["Interpolation", this.index])
next(); // skip `\<'
let line
while (now() !== ">") {
line = parseLine("I")
}
next(); // skip `>'
this.debugStack.pop()
return line
}
parseIdentifier(inside) {
this.debugStack.push(["Identifier", this.index])
let parts = []
idLoop:
while (1) {
switch (now()) {
case "\\":
parts[parts.length-1]+=parseEscape();
continue
case "<":
parts.push(
parseInterpolation()
);
parts.push("")
continue
case "}":
if (inside === "L")
break idLoop
break
case ">":
if (inside === "I")
break idLoop
break
case "#":
break idLoop
}
parts[parts.length-1]+=now();
next();
}
this.debugStack.pop();
return parts
}
}
// When the program starts:
onRun();
quit();