Compare commits

..

1 commit

Author SHA1 Message Date
c3d31cbb40 a LOT of refactoring and minor bugfixes 2026-01-05 22:19:20 -06:00
6 changed files with 463 additions and 112 deletions

View file

@ -2,7 +2,7 @@
#! nix-shell -i bash #! nix-shell -i bash
#! nix-shell -p bash nodejs jq #! nix-shell -p bash nodejs jq
node itty/utils/thread.js spool spinny.yarn spinny.js node itty/utils/thread.js spool spinny.yarn spinny.js s.spin r.spin
node itty/utils/thread.js metadata-from-json spinny.yarn yarnball-meta.json node itty/utils/thread.js metadata-from-json spinny.yarn yarnball-meta.json
#cat spinny.yarn mergefile.json | jq -s '.[0] as $old | .[1] as $merge | .[0].content | fromjson * $merge | tojson as $new | $old + {"content":$new} | tojson' | jq 'fromjson' > spinny.yarn.tmp #cat spinny.yarn mergefile.json | jq -s '.[0] as $old | .[1] as $merge | .[0].content | fromjson * $merge | tojson as $new | $old + {"content":$new} | tojson' | jq 'fromjson' > spinny.yarn.tmp

107
r.spin Normal file
View file

@ -0,0 +1,107 @@
out `Small ``Game\'\'\n'
use term
set `score' `0'
set `win.maxw' {
# ^ win.maxw is just the variable name. The dot is just a character.
# the dot doesn't mean anything special here.
# We doin RPN :O
sub `1' \
# `term:rows' is the name a command being run. \
# since variable names are quite permissive, this is just the name of a \
# command defined within the file `term' (imported at the top) \
# \
# Also note that a \ at the end of a line, even after a comment \
# continues a line. meaning, term:rows will be the seconc argument \
# of the subtraction above. \
term:rows
}
set `win.maxh' {sub `1' term:cols}
set `guy.char' `\U0001FBC5'
set `guy.x' `1'
set `guy.y' `1'
# Unicode is supported inside quotes:
set `star.char' `⭐'
# also non-ascii is allowed in identifiers:
set `\x1b' `hello'
# and escapes are allowed in the middle of identifiers:
out \x1b # << outputs hello
# and interpolation can be done on its own:
out <`\x1b'> # << also outputs hello
# and ofc, you could use ^[ in the raw code:
out  # << bad idea, but yes, it still writes hello.
# and then we'll undefine it for your sanity:
del `\x1b'
# `def' defines a command
def randstarpos {
set `star.x' {
# Get the real part of the rounded number after adding 1. (
# rounding removes decimal parts but leaves the immaginary
# part):
rea {rnd {add `1' {
mul win.maxw rnd
# ^ rnd returns a random number
# from 0 to 1
}}}
}
set `star.y' {rea {rnd
add `1' {mul rnd win.maxh}
}}
}
def w {set `guy.y' {rea {rnd {
max `1' {sub `1' guy.y}
}}}}
def a {set `guy.x' {rea {rnd {max `1' {sub `1' guy.x}}}}}
def s {set `guy.y' {rea {rnd {min win.maxh {add `1' guy.y}}}}}
def d {set `guy.x' {rea {rnd {min win.maxw {add `1' guy.x}}}}}
!randstarpos
inf !{
#^the infinite loop command.
#Note that the bang is nessicary as otherwise, the lazy would always
#inherit the same state.
out `\x1b[<rea {rnd guy.y}>;<rea {rnd guy.x}>H<guy.char>'
# ^writes to stdout
out `\x1b[<rea {rnd star.y}>;<rea {rnd star.x}>H<star.char>'
map inp \
`w' !w \
`k' !w \
`a' !a \
`j' !a \
`s' !s \
`h' !s \
`d' !d \
`l' !d \
defa {}
# ^map -- maps a value to a result
# The arguments alternate between
# things to match against and lazies
# to run as a result of those matches.
# The result is always a block that
# will be ccl'd if the match is found.
# The last thing to match against is
# always assumed to be the default.
# It is convention to make the last
# matching argument `defa'.
# `inp' gets a single char from stdin.
ifs {
eql `<guy.x>' `<star.x>'
# ^ eql will return `f' if they're
# not equal and `' if they're equal.
eql `<guy.y>' `<star.y>'
# Note that since output is combined, this block is effectively
# an and operator. The actual and operator is better tho. It
# provides short-circuting.
} then !{
!randstarpos
set score {add `1' score}
out `\x1b[2J\x1b[HScore: <rea {rnd score}>'
}
}

136
s.spin Normal file
View file

@ -0,0 +1,136 @@
out `Small ``Game\'\'\n'
use term
# Before we begin, a few notes
# - All objects are strings
# - There are a few different kinds of objects.
# - variables which have a static value they always evaluate to
# - lazies (contained by curlies (`{' and `}')) lazilly evaluate the lines
# within themselves only when nessicary. They may take arguments the same
# way commands do
# - commands, which are essentially lazies behind the mask of a variable name
# - strings, contained by a backtick to open (``') and a single quote to close
# (`\'').
# - Each line begins with any object
# - builtin commands are always 3 characters long
# - the starting object is followed by a space, then the arguments, which are
# space seperated objects
# - the arguments are passed to the first object
# - variables and strings ignore these arguments
# - commands and lazies may access these arguments with the `arg' command
# - interpolation, either within a variable name, command name, or string, is
# done with the `<...>' syntax.
# - The arrows, as they're called, evaluate the line inside them, then insert
# that value into the variable name/command name/string.
# - thus a lone `<varname>' on a line evaluates to the value of the variable
# that is named by varname.
# - To say that backwards, varname contains the name of a variable, and that
# variable has a value, and that value is what we're getting.
# - When a object begins with a bang (`!'), if object being evaluated sets any
# variables, it sets them within the current scope.
# - Variable and function names are stripped of bangs and spaces.
# - Booleans don't exist. Instead, any string that is empty (`') is truthy and
# all other (non-empty) strings are falsey.
# - Numbers are strings in the formats:
# - `[+\-]?real(\.dec)?[+\-]imag(\.dec)?i'
# - `[+\-]?real(\.dec)?'
# - `[+\-]?imag(\.dec)?i'
# - where, imag is the immaginary part, real is the real part, and the .dec is
# the decimal part of the one it appears to the right of.
# - These strings are in any base from 3 to 19.
# - bases 20+ are reserved since they would involve the letter i (which
# denotes the immaginary part)
# - the current base is defined by the 'base' variable, which is always in
# base 2.
# - base 2 is reserved for no good reason.
# - all bases lower than two are reserved because they're inconveinent to
# implement.
set `score' `0'
set `win.maxw' {
# ^ win.maxw is just the variable name. The dot is just a character.
# the dot doesn't mean anything special here.
# We doin RPN :O
sub `1' term:rows
# `term:rows' is the name a command being run.
# since variable names are quite permissive, this is just the name of a
# command defined within the file `term' (imported at the top)
}
set `win.maxh' {sub `1' term:cols}
set `guy.char' `\U0001FBC5'
set `guy.x' `1'
set `guy.y' `1'
# Unicode is supported inside quotes:
set `star.char' `⭐'
# `def' defines a command
def randstarpos {
set `star.x' {
# Get the real part of the rounded number after adding 1. (
# rounding removes decimal parts but leaves the immaginary
# part):
rea {rnd {add `1' {
mul win.maxw rnd
# ^ rnd returns a random number
# from 0 to 1
}}}
}
set `star.y' {rea {rnd
add `1' {mul rnd win.maxh}
}}
}
def w {set `guy.y' {rea {rnd {
max `1' {sub `1' guy.y}
}}}}
def a {set `guy.x' {rea {rnd {max `1' {sub `1' guy.x}}}}}
def s {set `guy.y' {rea {rnd {min win.maxh {add `1' guy.y}}}}}
def d {set `guy.x' {rea {rnd {min win.maxw {add `1' guy.x}}}}}
!randstarpos
inf !{
#^the infinite loop command.
#Note that the bang is nessicary as otherwise, the lazy would always
#inherit the same state.
out `\x1b[<rea {rnd guy.y}>;<rea {rnd guy.x}>H<guy.char>'
# ^writes to stdout
out `\x1b[<rea {rnd star.y}>;<rea {rnd star.x}>H<star.char>'
map inp \
`w' !w \
`k' !w \
`a' !a \
`j' !a \
`s' !s \
`h' !s \
`d' !d \
`l' !d \
defa {}
# ^map -- maps a value to a result
# The arguments alternate between
# things to match against and lazies
# to run as a result of those matches.
# The result is always a block that
# will be ccl'd if the match is found.
# The last thing to match against is
# always assumed to be the default.
# It is convention to make the last
# matching argument `defa'.
# `inp' gets a single char from stdin.
ifs {
eql `<guy.x>' `<star.x>'
# ^ eql will return `f' if they're
# not equal and `' if they're equal.
eql `<guy.y>' `<star.y>'
# Note that since output is combined, this block is effectively
# an and operator. The actual and operator is better tho. It
# provides short-circuting.
} then !{
!randstarpos
set score {add `1' score}
out `\x1b[2J\x1b[HScore: <rea {rnd score}>'
}
}

324
spinny.js
View file

@ -8,35 +8,48 @@
// - Spinny source files (.spin) // - Spinny source files (.spin)
// - Spinny compiled json files (.spic) // - Spinny compiled json files (.spic)
function onRun() { try {
async function onRun() {
let cliArgs = parseCliArgs(); 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) { } catch (e) {
display.print(`Spinny: Error opening file or resolving path \`${cliArgs.filename}'.`); 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"]); io.error([22, "file open failed"]);
} }
try { const file = fileobj.read(); try {
file = fileobj.read();
} catch (e) { } catch (e) {
display.print(`Spinny: Error reading file \`${cliArgs.filename}'.`); display.print(`Spinny: Error reading file \`${cliArgs.filename}'.`);
display.print(`${e.name}@${e.lineNumber}-${e.columnNumber}: ${e.message}`);
io.error([23, "file read failed"]); io.error([23, "file read failed"]);
} }
const fileSpincVersion = file.split("\n")[1].split("@");
// Spinc Checker:
let isSpincFile = false; let isSpincFile = false;
if (fileSpincVersion.slice(0,2).join("\n") === "SPINC\nspinny:itty") { if (file.split("\n").length > 1) {
// This is a spinny source file const fileSpincVersion = file.split("\n")[1].split("@");
if (fileSpincVersion[2] !== "noversion") { if (fileSpincVersion.length > 2 &&
let niceversion = spileSpincVersion[2].replace( fileSpincVersion.slice(0,2).join("\n") === "SPINC\nspinny:itty") {
/[\t %]/g , "") // This is a spinny source file
display.print("Spinny: Incorrect or unrecognized compiled spinc file"); if (fileSpincVersion[2] !== "noversion") {
display.print(`Spinny: version \`${niceversion}'.`); let niceversion = spileSpincVersion[2].replace(
display.print("Spinny: This version of the spinny interpreter exclusively"); /[\t %]/g , "")
display.print("Spinny: accepts compiled spinc files of version"); display.print("Spinny: Incorrect or unrecognized compiled spinc file");
display.print("Spinny: `noversion'."); display.print(`Spinny: version \`${niceversion}'.`);
display.print("Spinny: Use `—version' or `—help' for more information."); display.print("Spinny: This version of the spinny interpreter exclusively");
io.error([31, "invalid spinc version"]); 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) { if (cliArgs.compileOnly && isSpincFile) {
display.print("Spinny: Cannot compile a spinc file."); display.print("Spinny: Cannot compile a spinc file.");
@ -44,18 +57,32 @@ function onRun() {
io.error([32, "spinc already compiled"]); io.error([32, "spinc already compiled"]);
} }
let compiled
if (!isSpincFile) { if (!isSpincFile) {
// We must compile the file // We must compile the file
const parser = new spinnyParser; const parser = new spinnyParser({
const compiled = parser.compile(file) 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: Running and/or saving files is not currently supported.");
display.print("Spinny: Dumping contents instead."); display.print("Spinny: Dumping contents instead.");
if (isSpincFile) if (isSpincFile)
display.print(file.split("\n").slice(2).join('\n')) display.print(file.split("\n").slice(2).join('\n'))
else
display.print(compiled)
} }
function showHelp(){ function showHelp(){
@ -70,25 +97,25 @@ function showHelp(){
"Syntax:\n" + "Syntax:\n" +
`\t${program.name} [flags...] [—] <filename>\n` + `\t${program.name} [flags...] [—] <filename>\n` +
"Where:\n" + "Where:\n" +
"\t[flags...] are any combination of the flags\n" + "\t[flags...] are any combination of the flags\n" +
"\t available in the `Flags' section\n" + "\t available in the `Flags' section\n" +
"\t below.\n" + "\t below.\n" +
"\t[—] Is an emdash (—) or a double dash (--)\n" + "\t[—] Is an emdash (—) or a double dash (--)\n" +
"\t to mark where the filename is supposed\n" + "\t to mark where the filename is supposed\n" +
"\t to go. (Note that any time you see an\n" + "\t to go. (Note that any time you see an\n" +
"\t em dash in this help page, you may\n" + "\t em dash in this help page, you may\n" +
"\t choose to use a double dash in its\n" + "\t choose to use a double dash in its\n" +
"\t place if that is your preference.)\n" + "\t place if that is your preference.)\n" +
"\t<filename> is the `.spin' or `.spinc' file to\n" + "\t<filename> is the `.spin' or `.spinc' file to\n" +
"\t compile/run\n" + "\t compile/run\n" +
"Flags:\n" + "Flags:\n" +
"\t—help Print this help page and exit.\n" + "\t—help Print this help page and exit.\n" +
"\t—version Print version and exit.\n" + "\t—version Print version and exit.\n" +
"\t—compile Compile the code to a .spinc file.\n" + "\t—compile Compile the code to a .spinc file.\n" +
"\t The .spinc file must be specified\n" + "\t The .spinc file must be specified\n" +
"\t with `—output'.\n" + "\t with `—output'.\n" +
"\t—output <file> Specifies the output file for the\n" + "\t—output <file> Specifies the output file for the\n" +
"\t `—compile' flag.\n" "\t `—compile' flag.\n"
); );
}; };
@ -105,18 +132,19 @@ function showVersion(){
function parseCliArgs() { function parseCliArgs() {
let compileOnly = false; let compileOnly = false;
let filename = ""; let filename = null;
let noMoreFlags = false; let noMoreFlags = false;
let nextIsOutput = false let nextIsOutput = false
let output = ""; let output = "";
let letterFlag = false let letterFlag = false
let flag = false let flag = false
let debugStep = false
for (let i=0;i<args.length;i++) { for (let i=0;i<args.length;i++) {
let arg = args[i]; let arg = args[i];
if (nextIsOutput) { if (nextIsOutput) {
output = arg; output = arg;
nextIsOutput = false; nextIsOutput = false;
continue;
} }
if (!noMoreFlags) { if (!noMoreFlags) {
// Flags // Flags
@ -133,20 +161,31 @@ function parseCliArgs() {
letterFlag = true; letterFlag = true;
arg = arg.slice(1); arg = arg.slice(1);
} }
if (flag) { if (flag) { switch (arg) {
if (arg === "compile" || arg === "c") { case "c":
case "compile":
compileOnly = true; compileOnly = true;
} else if (arg === "help" || arg === "h") { break;
case "h":
case "help":
showHelp(); showHelp();
quit(); quit();
} else if (arg === "version" || arg === "v") { case "v":
case "version":
showVersion(); showVersion();
quit(); quit();
} else if (arg === "output" || arg === "o") { case "o":
case "output":
nextIsOutput = true; nextIsOutput = true;
} else if (arg === "" && !letterFlag) { break
noMoreFlags = true; case "debugstep":
} else { debugStep = true;
case "":
if (!letterFlag) {
noMoreFlags = true;
break
}
default:
if (letterFlag){ if (letterFlag){
display.print(`Spinny: Unrecognized flag \`-${arg}'.`); display.print(`Spinny: Unrecognized flag \`-${arg}'.`);
} else { } else {
@ -154,19 +193,30 @@ function parseCliArgs() {
} }
display.print("Spinny: For allowed flags, use `—help'."); display.print("Spinny: For allowed flags, use `—help'.");
io.error([11, "unknown flag"]); io.error([11, "unknown flag"]);
} }; continue; }
continue;
}
} }
filename = arg; 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 { return {
filename: filename, filename: filename,
compileOnly: compileOnly, 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 { class spinnyParser {
INSIDE = { INSIDE = {
@ -174,14 +224,17 @@ class spinnyParser {
INTER: 1, INTER: 1,
LAZY: 2 LAZY: 2
} }
compile(sourceFileContents) { constructor(modifiers=null) {
this.mods = modifiers
}
async compile(sourceFileContents) {
this.file = sourceFileContents; this.file = sourceFileContents;
this.index = 0; this.index = 0;
this.debugStack = []; this.debugStack = [];
return this.parseFile() return await this.parseFile()
} }
parseFile() { async parseFile() {
this.debugStack.push(["File", this.index]); await this.tag("File")
let lines = []; let lines = [];
// A bunch of lines // A bunch of lines
@ -190,18 +243,56 @@ class spinnyParser {
this.next(); this.next();
continue continue
} }
lines.push(this.parseLine(INSIDE.FILE)) lines.push(await this.parseLine(this.INSIDE.FILE))
} }
this.debugStack.pop(); await this.untag();
return lines return lines
} }
// Utility Functions
now() { return this.file[this.index] } 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] } 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) { // Parsing Functions
this.debugStack.push(["Line", this.index]) async parseLine(inside) {
await this.tag("Line")
let words = [] let words = []
wordLoop: wordLoop:
while (1) { while (1) {
@ -213,19 +304,19 @@ class spinnyParser {
this.next(); this.next();
continue continue
case "#": case "#":
this.parseComment(); await this.parseComment();
continue continue
case "}": case "}":
if (inside === INSIDE.LAZY) if (inside === this.INSIDE.LAZY)
break wordLoop break wordLoop
break break
case ">": case ">":
if (inside === INSIDE.INTER) if (inside === this.INSIDE.INTER)
break wordLoop break wordLoop
break break
case "\n": case "\n":
if (inside === INSIDE.FILE if (inside === this.INSIDE.FILE
|| inside == INSIDE.LAZY) || inside == this.INSIDE.LAZY)
break wordLoop break wordLoop
break break
case "\\": case "\\":
@ -238,14 +329,14 @@ class spinnyParser {
} }
break break
} }
words.push(this.parseWord(inside)); words.push(await this.parseWord(inside));
} }
this.debugStack.pop() await this.untag()
return words return words
} }
parseComment() { async parseComment() {
this.debugStack.push(["Comment", this.index]) await this.tag("Comment")
commentLoop: commentLoop:
while (1) { while (1) {
switch (this.now()) { switch (this.now()) {
@ -258,11 +349,11 @@ class spinnyParser {
this.next(); this.next();
} }
} }
this.debugStack.pop() await this.untag()
} }
parseWord(inside) { async parseWord(inside) {
this.debugStack.push(["Word", this.index]) await this.tag("Word")
let bang = false; let bang = false;
let word = {} let word = {}
word[CMPVER.KEYS.BANG] = false; word[CMPVER.KEYS.BANG] = false;
@ -273,41 +364,47 @@ class spinnyParser {
switch (this.now()) { switch (this.now()) {
case "{": case "{":
word[CMPVER.KEYS.TYPE] = CMPVER.TYPE.LAZY; word[CMPVER.KEYS.TYPE] = CMPVER.TYPE.LAZY;
word[CMPVER.KEYS.LINES] = this.parseLazy(); word[CMPVER.KEYS.LINES] = await this.parseLazy();
break;
case "`": case "`":
word[CMPVER.KEYS.TYPE] = CMPVER.TYPE.LIT; word[CMPVER.KEYS.TYPE] = CMPVER.TYPE.LIT;
word[CMPVER.KEYS.PARTS] = this.parseLiteral(); word[CMPVER.KEYS.PARTS] = await this.parseLiteral();
break;
default: default:
word[CMPVER.KEYS.TYPE] = CMPVER.TYPE.ID; 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 return word
} }
parseLazy() { async parseLazy() {
this.debugStack.push(["Lazy", this.index]) await this.tag("Lazy")
this.next(); // Skip `{' this.next(); // Skip `{'
let lines = [] let lines = []
while (this.now() !== "}") { 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 return lines
} }
parseLiteral() { async parseLiteral() {
this.debugStack.push(["Literal", this.index]) await this.tag("Literal")
this.next(); // Skip `\`' this.next(); // Skip `\`'
let parts = [""] let parts = [""]
while (this.now() !== "\'") { while (this.now() !== "\'") {
switch (this.now()) { switch (this.now()) {
case "\\": case "\\":
parts[parts.length-1]+=this.parseEscape(); parts[parts.length-1]+=await this.parseEscape();
break break
case "<": case "<":
parts.push( parts.push(
this.parseInterpolation() await this.parseInterpolation()
); );
parts.push("") parts.push("")
break break
@ -317,12 +414,13 @@ class spinnyParser {
break break
} }
} }
this.debugStack.pop(); this.next(); // Skip `\''
await this.untag();
return parts return parts
} }
parseEscape() { async parseEscape() {
this.debugStack.push(["Literal", this.index]) await this.tag("Escape")
this.next(); // skip `\\' this.next(); // skip `\\'
let out = ""; let out = "";
switch (this.now()) { switch (this.now()) {
@ -366,54 +464,57 @@ class spinnyParser {
out = this.now(); // record the thing after the `\\' out = this.now(); // record the thing after the `\\'
this.next(); // skip over it this.next(); // skip over it
break break
} }
this.debugStack.pop(); await this.untag();
return out; return out;
} }
parseInterpolation() { async parseInterpolation() {
this.debugStack.push(["Interpolation", this.index]) await this.tag("Interpolation")
this.next(); // skip `\<' this.next(); // skip `\<'
let line let line
while (this.now() !== ">") { while (this.now() !== ">") {
line = this.parseLine(INSIDE.INTER) line = await this.parseLine(this.INSIDE.INTER)
} }
this.next(); // skip `>' this.next(); // skip `>'
this.debugStack.pop() await this.untag()
return line return line
} }
parseIdentifier(inside) { async parseIdentifier(inside) {
this.debugStack.push(["Identifier", this.index]) await this.tag("Identifier")
let parts = [""] let parts = [""]
idLoop: idLoop:
while (1) { while (1) {
switch (this.now()) { switch (this.now()) {
case "\\": case "\\":
parts[parts.length-1]+=this.parseEscape(); parts[parts.length-1]+=await this.parseEscape();
continue continue
case "<": case "<":
parts.push( parts.push(
this.parseInterpolation() await this.parseInterpolation()
); );
parts.push("") parts.push("")
continue continue
case "}": case "}":
if (inside === INSIDE.LAZY) if (inside === this.INSIDE.LAZY)
break idLoop break idLoop
break break
case ">": case ">":
if (inside === INSIDE.INTER) if (inside === this.INSIDE.INTER)
break idLoop break idLoop
break break
case "#": case "#":
case " ":
case "\t":
case "\n":
break idLoop break idLoop
} }
parts[parts.length-1]+=this.now(); parts[parts.length-1]+=this.now();
this.next(); this.next();
} }
this.debugStack.pop(); await this.untag();
return parts return parts
} }
} }
@ -437,5 +538,10 @@ const SPIDNEY = {//spinny identifiers
const CMPVER = SPIDNEY["noversion"] //compileversion const CMPVER = SPIDNEY["noversion"] //compileversion
// When the program starts: // When the program starts:
onRun(); await onRun();
quit(); 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"]);
}

File diff suppressed because one or more lines are too long

View file

@ -4,6 +4,8 @@
"version": [0,0,3], "version": [0,0,3],
"dependencies": [], "dependencies": [],
"paths": { "paths": {
"spinny.js": "%B/spinny.js" "spinny.js": "%B/spinny.js",
"s.spin": "/s.spin",
"r.spin": "/r.spin"
} }
} }