a LOT of refactoring and minor bugfixes

This commit is contained in:
Mewrry the Kitty 2026-01-05 22:19:20 -06:00
parent f4669290b0
commit c3d31cbb40
6 changed files with 463 additions and 112 deletions

324
spinny.js
View file

@ -8,35 +8,48 @@
// - Spinny source files (.spin)
// - Spinny compiled json files (.spic)
function onRun() {
try {
async function onRun() {
let cliArgs = parseCliArgs();
let fileobj
let file
try { const fileobj = io.open(fs.resolve(cliArgs.filename), "r")
// 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 { const file = fileobj.read();
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"]);
}
const fileSpincVersion = file.split("\n")[1].split("@");
// Spinc Checker:
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"]);
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;
}
isSpincFile = true;
}
if (cliArgs.compileOnly && isSpincFile) {
display.print("Spinny: Cannot compile a spinc file.");
@ -44,18 +57,32 @@ function onRun() {
io.error([32, "spinc already compiled"]);
}
let compiled
if (!isSpincFile) {
// We must compile the file
const parser = new spinnyParser;
const compiled = parser.compile(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'))
else
display.print(compiled)
}
function showHelp(){
@ -70,25 +97,25 @@ function showHelp(){
"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" +
"\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"
"\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"
);
};
@ -105,18 +132,19 @@ function showVersion(){
function parseCliArgs() {
let compileOnly = false;
let filename = "";
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
@ -133,20 +161,31 @@ function parseCliArgs() {
letterFlag = true;
arg = arg.slice(1);
}
if (flag) {
if (arg === "compile" || arg === "c") {
if (flag) { switch (arg) {
case "c":
case "compile":
compileOnly = true;
} else if (arg === "help" || arg === "h") {
break;
case "h":
case "help":
showHelp();
quit();
} else if (arg === "version" || arg === "v") {
case "v":
case "version":
showVersion();
quit();
} else if (arg === "output" || arg === "o") {
case "o":
case "output":
nextIsOutput = true;
} else if (arg === "" && !letterFlag) {
noMoreFlags = true;
} else {
break
case "debugstep":
debugStep = true;
case "":
if (!letterFlag) {
noMoreFlags = true;
break
}
default:
if (letterFlag){
display.print(`Spinny: Unrecognized flag \`-${arg}'.`);
} else {
@ -154,19 +193,30 @@ function parseCliArgs() {
}
display.print("Spinny: For allowed flags, use `—help'.");
io.error([11, "unknown flag"]);
}
continue;
}
}; 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
output: output,
debugStep: debugStep
};
}
class spinnyCompileError extends Error {
constructor(message, debugStack) {
super(message);
this.name = "spinnyCompileError";
this.debugStack = debugStack;
}
}
class spinnyParser {
INSIDE = {
@ -174,14 +224,17 @@ class spinnyParser {
INTER: 1,
LAZY: 2
}
compile(sourceFileContents) {
constructor(modifiers=null) {
this.mods = modifiers
}
async compile(sourceFileContents) {
this.file = sourceFileContents;
this.index = 0;
this.debugStack = [];
return this.parseFile()
return await this.parseFile()
}
parseFile() {
this.debugStack.push(["File", this.index]);
async parseFile() {
await this.tag("File")
let lines = [];
// A bunch of lines
@ -190,18 +243,56 @@ class spinnyParser {
this.next();
continue
}
lines.push(this.parseLine(INSIDE.FILE))
lines.push(await this.parseLine(this.INSIDE.FILE))
}
this.debugStack.pop();
await this.untag();
return lines
}
// Utility Functions
now() { return this.file[this.index] }
next() { 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];
}
parseLine(inside) {
this.debugStack.push(["Line", this.index])
// Parsing Functions
async parseLine(inside) {
await this.tag("Line")
let words = []
wordLoop:
while (1) {
@ -213,19 +304,19 @@ class spinnyParser {
this.next();
continue
case "#":
this.parseComment();
await this.parseComment();
continue
case "}":
if (inside === INSIDE.LAZY)
if (inside === this.INSIDE.LAZY)
break wordLoop
break
case ">":
if (inside === INSIDE.INTER)
if (inside === this.INSIDE.INTER)
break wordLoop
break
case "\n":
if (inside === INSIDE.FILE
|| inside == INSIDE.LAZY)
if (inside === this.INSIDE.FILE
|| inside == this.INSIDE.LAZY)
break wordLoop
break
case "\\":
@ -238,14 +329,14 @@ class spinnyParser {
}
break
}
words.push(this.parseWord(inside));
words.push(await this.parseWord(inside));
}
this.debugStack.pop()
await this.untag()
return words
}
parseComment() {
this.debugStack.push(["Comment", this.index])
async parseComment() {
await this.tag("Comment")
commentLoop:
while (1) {
switch (this.now()) {
@ -258,11 +349,11 @@ class spinnyParser {
this.next();
}
}
this.debugStack.pop()
await this.untag()
}
parseWord(inside) {
this.debugStack.push(["Word", this.index])
async parseWord(inside) {
await this.tag("Word")
let bang = false;
let word = {}
word[CMPVER.KEYS.BANG] = false;
@ -273,41 +364,47 @@ class spinnyParser {
switch (this.now()) {
case "{":
word[CMPVER.KEYS.TYPE] = CMPVER.TYPE.LAZY;
word[CMPVER.KEYS.LINES] = this.parseLazy();
word[CMPVER.KEYS.LINES] = await this.parseLazy();
break;
case "`":
word[CMPVER.KEYS.TYPE] = CMPVER.TYPE.LIT;
word[CMPVER.KEYS.PARTS] = this.parseLiteral();
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] = this.parseIdentifier(inside);
word[CMPVER.KEYS.PARTS] = await this.parseIdentifier(inside);
}
this.debugStack.pop()
await this.untag()
return word
}
parseLazy() {
this.debugStack.push(["Lazy", this.index])
async parseLazy() {
await this.tag("Lazy")
this.next(); // Skip `{'
let lines = []
while (this.now() !== "}") {
lines.push(this.parseLine(INSIDE.LAZY))
lines.push(await this.parseLine(this.INSIDE.LAZY))
if (this.now() === "\n")
// Continue to next line like a file would
this.next();
}
this.debugStack.pop()
this.next(); // Skip `}'
await this.untag()
return lines
}
parseLiteral() {
this.debugStack.push(["Literal", this.index])
async parseLiteral() {
await this.tag("Literal")
this.next(); // Skip `\`'
let parts = [""]
while (this.now() !== "\'") {
switch (this.now()) {
case "\\":
parts[parts.length-1]+=this.parseEscape();
parts[parts.length-1]+=await this.parseEscape();
break
case "<":
parts.push(
this.parseInterpolation()
await this.parseInterpolation()
);
parts.push("")
break
@ -317,12 +414,13 @@ class spinnyParser {
break
}
}
this.debugStack.pop();
this.next(); // Skip `\''
await this.untag();
return parts
}
parseEscape() {
this.debugStack.push(["Literal", this.index])
async parseEscape() {
await this.tag("Escape")
this.next(); // skip `\\'
let out = "";
switch (this.now()) {
@ -366,54 +464,57 @@ class spinnyParser {
out = this.now(); // record the thing after the `\\'
this.next(); // skip over it
break
}
this.debugStack.pop();
await this.untag();
return out;
}
parseInterpolation() {
this.debugStack.push(["Interpolation", this.index])
async parseInterpolation() {
await this.tag("Interpolation")
this.next(); // skip `\<'
let line
while (this.now() !== ">") {
line = this.parseLine(INSIDE.INTER)
line = await this.parseLine(this.INSIDE.INTER)
}
this.next(); // skip `>'
this.debugStack.pop()
await this.untag()
return line
}
parseIdentifier(inside) {
this.debugStack.push(["Identifier", this.index])
async parseIdentifier(inside) {
await this.tag("Identifier")
let parts = [""]
idLoop:
while (1) {
switch (this.now()) {
case "\\":
parts[parts.length-1]+=this.parseEscape();
parts[parts.length-1]+=await this.parseEscape();
continue
case "<":
parts.push(
this.parseInterpolation()
await this.parseInterpolation()
);
parts.push("")
continue
case "}":
if (inside === INSIDE.LAZY)
if (inside === this.INSIDE.LAZY)
break idLoop
break
case ">":
if (inside === INSIDE.INTER)
if (inside === this.INSIDE.INTER)
break idLoop
break
case "#":
case " ":
case "\t":
case "\n":
break idLoop
}
parts[parts.length-1]+=this.now();
this.next();
}
this.debugStack.pop();
await this.untag();
return parts
}
}
@ -437,5 +538,10 @@ const SPIDNEY = {//spinny identifiers
const CMPVER = SPIDNEY["noversion"] //compileversion
// When the program starts:
onRun();
quit();
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"]);
}