-
-
Save mousetail/e4bb725619c8832ef8e3c82084d8ddd8 to your computer and use it in GitHub Desktop.
Bubbler version of Knight, forked from Seffan
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const fs = require('node:fs'); | |
const input = String.fromCharCode(...fs.readFileSync(0)).split('\n'); | |
const source = fs.readFileSync('source.knight', 'utf8'); | |
const Knight = (() => { | |
let inputs = []; | |
let output = ''; | |
class KnightError extends Error {} | |
class ParseError extends KnightError {} | |
class RuntimeError extends KnightError {} | |
class Stream { | |
constructor(source) { | |
this.source = source; | |
} | |
stripWhitespace() { | |
this.match(/^([\]\[\s(){}:]+|#[^\n]*(\n|$))*/); | |
} | |
peek() { | |
return this.source[0] || null; | |
} | |
match(regex, group = 0) { | |
const match = regex.exec(this.source); | |
if (match === null) { | |
return null; | |
} | |
this.source = this.source.substr(match[0].length); | |
return match[group]; | |
} | |
toString() { | |
return this.source; | |
} | |
} | |
const TYPES = []; | |
class Value { | |
static parse(stream) { | |
stream.stripWhitespace(); | |
for (var i = 0; i < TYPES.length; i++) { | |
const match = TYPES[i].parse(stream); | |
if (match) { | |
return match; | |
} | |
} | |
return null; | |
} | |
run() { | |
throw new Error(); | |
} | |
debug() { | |
throw new Error(); | |
} | |
toString() { | |
return this.run().toString(); | |
} | |
toNumber() { | |
return this.run().toNumber(); | |
} | |
toBoolean() { | |
return this.run().toBoolean(); | |
} | |
} | |
class Literal extends Value { | |
_data; | |
constructor(data) { | |
super(); | |
this._data = data; | |
} | |
run() { | |
return this; | |
} | |
toString() { | |
return String(this._data); | |
} | |
toNumber() { | |
return Number(this._data); | |
} | |
toBoolean() { | |
return Boolean(this._data); | |
} | |
eql(rhs) { | |
return rhs instanceof this.constructor && this._data === rhs._data; | |
} | |
} | |
class Bool extends Literal { | |
static parse(stream) { | |
const match = stream.match(/^([TF])[A-Z]*/, 1); | |
return match && new Bool(match === "T"); | |
} | |
dump() { | |
return `Boolean(${this})`; | |
} | |
lth(rhs) { | |
return !this._data && rhs.toBoolean(); | |
} | |
gth(rhs) { | |
return this._data && !rhs.toBoolean(); | |
} | |
} | |
TYPES.push(Bool); | |
const ENVIRONMENT = {}; | |
class Ident extends Value { | |
static parse(stream) { | |
const match = stream.match(/^[a-z_][a-z0-9_]*/); | |
return match && new Ident(match); | |
} | |
constructor(ident) { | |
super(); | |
this.ident = ident; | |
} | |
dump() { | |
return `Identifier(${this.ident})`; | |
} | |
assign(value) { | |
ENVIRONMENT[this.ident] = value; | |
} | |
run() { | |
const value = ENVIRONMENT[this.ident]; | |
if (value === undefined) { | |
throw new RuntimeError(`Unknown identifier '${this.ident}'`); | |
} else { | |
return value; | |
} | |
} | |
} | |
TYPES.push(Ident); | |
class Int extends Literal { | |
static parse(stream) { | |
const match = stream.match(/^\d+/); | |
return match && new Int(Number(match)); | |
} | |
dump() { | |
return `Number(${this})`; | |
} | |
add(rhs) { | |
return new Int(this._data + rhs.toNumber()); | |
} | |
sub(rhs) { | |
return new Int(this._data - rhs.toNumber()); | |
} | |
mul(rhs) { | |
return new Int(this._data * rhs.toNumber()); | |
} | |
div(rhs) { | |
const rhsInt = rhs.toNumber(); | |
if (rhsInt === 0) { | |
throw new RuntimeError("Cannot divide by zero"); | |
} else { | |
return new Int(Math.trunc(this._data / rhsInt)); | |
} | |
} | |
mod(rhs) { | |
const rhsInt = rhs.toNumber(); | |
if (rhsInt === 0) { | |
throw new RuntimeError("Cannot modulo by zero"); | |
} else { | |
return new Int(this._data % rhsInt); | |
} | |
} | |
pow(rhs) { | |
const rhsInt = rhs.toNumber(); | |
if (this._data === 0 && rhsInt < 0) { | |
throw new RuntimeError("Cannot exponentiate zero to a negative power"); | |
} else { | |
return new Int(Math.trunc(this._data ** rhsInt)); | |
} | |
} | |
lth(rhs) { | |
return this._data < rhs.toNumber(); | |
} | |
gth(rhs) { | |
return this._data > rhs.toNumber(); | |
} | |
} | |
TYPES.push(Int); | |
class Null extends Literal { | |
static parse(stream) { | |
return stream.match(/^N[A-Z]*/) && new Null(); | |
} | |
constructor() { | |
super(null); | |
} | |
dump() { | |
return "Null()"; | |
} | |
eql(rhs) { | |
return rhs instanceof Null; | |
} | |
lth(_rhs) { | |
throw new RuntimeError("Cannot compare Null."); | |
} | |
gth(_rhs) { | |
throw new RuntimeError("Cannot compare Null."); | |
} | |
} | |
TYPES.push(Null); | |
class Str extends Literal { | |
static parse(stream) { | |
const match = stream.match(/^(["'])([\s\S]*?)\1/, 2); | |
if (match !== null) { | |
return new Str(match); | |
} | |
const first = stream.peek(); | |
if (first === "'" || first === '"') { | |
throw new ParseError(`Unterminated quote encountered: ${stream}`); | |
} | |
} | |
toNumber() { | |
return parseInt(this._data, 10) || 0; | |
} | |
dump() { | |
return `String(${this})`; | |
} | |
add(rhs) { | |
return new Str(`${this}${rhs}`); | |
} | |
mul(rhs) { | |
return new Str(this._data.repeat(rhs.toNumber())); | |
} | |
lth(rhs) { | |
return this._data < rhs.toString(); | |
} | |
gth(rhs) { | |
return this._data > rhs.toString(); | |
} | |
} | |
TYPES.push(Str); | |
const FUNCTIONS = {}; | |
class Func extends Value { | |
static parse(stream) { | |
const front = stream.peek(); | |
const func = FUNCTIONS[front]; | |
if (func === undefined) { | |
return null; | |
} | |
stream.match(/^(?:[A-Z]+|.)/); | |
let args = []; | |
for (let i = 0; i < func.length; i++) { | |
const arg = Value.parse(stream); | |
if (!arg) { | |
throw new ParseError(`Missing argument ${i + 1} for func '${front}'`); | |
} | |
args.push(arg); | |
} | |
return new Func(func, front, args); | |
} | |
constructor(func, name, args) { | |
super(); | |
this.func = func; | |
this.name = name; | |
this.args = args; | |
} | |
run() { | |
return this.func(...this.args); | |
} | |
dump() { | |
let ret = "Function(" + this.name; | |
for (let val of this.args) { | |
ret += ", " + val.dump(); | |
} | |
return ret + ")"; | |
} | |
eql(rhs) { | |
return Object.is(this, rhs); | |
} | |
} | |
TYPES.push(Func); | |
function register(name, func) { | |
if (name.length !== 1) { | |
throw new Error("Name must be exactly one character long."); | |
} | |
FUNCTIONS[name] = func; | |
} | |
register("P", () => { | |
return inputs.length ? new Str(inputs.shift()) : new Null(); | |
}); | |
register("R", () => new Int(Math.floor(Math.random() * 0x1_0000_0000))); | |
register("E", (string) => evalRun(string.toString())); | |
register("A", (a) => { | |
const b = a.run(); | |
return b instanceof Int | |
? new Str(String.fromCharCode(b.toNumber())) | |
: new Int(b.toString().charCodeAt(0)); | |
}); | |
register("~", (a) => new Int(-a.toNumber())); | |
register("B", (block) => block); | |
register("C", (block) => block.run().run()); | |
register("`", (command) => { | |
throw new RuntimeError('Run shell command not supported on DSO.'); | |
}); | |
register("Q", (status) => { | |
throw new RuntimeError('Exit not supported on DSO.'); | |
}); | |
register("!", (arg) => new Bool(!arg.toBoolean())); | |
register("L", (str) => new Int(str.toString().length)); | |
register("D", (value) => { | |
const result = value.run(); | |
output += result.dump(); | |
return result; | |
}); | |
register("O", (input) => { | |
const str = input.toString(); | |
if (str.substr(-1) === "\\") { | |
output += str.substr(0, str.length - 1); | |
} else { | |
output += `${str}\n`; | |
} | |
return new Null(); | |
}); | |
register("+", (lhs, rhs) => lhs.run().add(rhs.run())); | |
register("-", (lhs, rhs) => lhs.run().sub(rhs.run())); | |
register("*", (lhs, rhs) => lhs.run().mul(rhs.run())); | |
register("/", (lhs, rhs) => lhs.run().div(rhs.run())); | |
register("%", (lhs, rhs) => lhs.run().mod(rhs.run())); | |
register("^", (lhs, rhs) => lhs.run().pow(rhs.run())); | |
register("<", (lhs, rhs) => new Bool(lhs.run().lth(rhs.run()))); | |
register(">", (lhs, rhs) => new Bool(lhs.run().gth(rhs.run()))); | |
register("?", (lhs, rhs) => new Bool(lhs.run().eql(rhs.run()))); | |
register("&", (lhs, rhs) => { | |
lhs = lhs.run(); | |
return lhs.toBoolean() ? rhs.run() : lhs; | |
}); | |
register("|", (lhs, rhs) => { | |
lhs = lhs.run(); | |
return lhs.toBoolean() ? lhs : rhs.run(); | |
}); | |
register(";", (lhs, rhs) => { | |
lhs.run(); | |
return rhs.run(); | |
}); | |
register("=", (ident, value) => { | |
if (!(ident instanceof Ident)) { | |
ident = new Ident(ident.toString()); | |
} | |
ident.assign((value = value.run())); | |
return value; | |
}); | |
register("W", (condition, body) => { | |
while (condition.toBoolean()) { | |
body.run(); | |
} | |
return new Null(); | |
}); | |
register("I", (cond, iftrue, iffalse) => { | |
return cond.toBoolean() ? iftrue.run() : iffalse.run(); | |
}); | |
register("G", (str, start, len) => { | |
str = str.toString(); | |
start = start.toNumber(); | |
len = len.toNumber(); | |
return new Str(str.substr(start, len) || ""); | |
}); | |
register("S", (str, start, len, repl) => { | |
str = str.toString(); | |
start = start.toNumber(); | |
len = len.toNumber(); | |
repl = repl.toString(); | |
if (str.length == start) { | |
return new Str(str + repl); | |
} | |
return new Str(str.substr(0, start) + repl + str.substr(start + len)); | |
}); | |
function evalRun(input) { | |
const value = Value.parse(new Stream(input.toString())); | |
if (value === null) { | |
throw new ParseError("No value could be parsed!"); | |
} else { | |
return value.run(); | |
} | |
} | |
function run(input, inps = []) { | |
inputs = [...inps]; | |
output = ''; | |
const value = Value.parse(new Stream(input.toString())); | |
if (value === null) { | |
throw new ParseError("No value could be parsed!"); | |
} else { | |
value.run(); | |
return output; | |
} | |
} | |
return { run }; | |
})(); | |
const output = Knight.run(source, input); | |
fs.writeFileSync(1, output, 'latin1'); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment