//Gods-awful type-checking logic.
//TypeName: string, Params: Array<string>
function Type(TypeName,Params=[]) {
if (!Type.all) Type.all = {};
this.typename = "";
this.mode = "";
if (TypeName instanceof Type) {
const t = TypeName.copy();
for (const k in t) {
this[k] = t[k];
}
return;
}
//Union/Tuple
if (/^\(.+\)$/.test(TypeName)) {
const types = [];
var depth = 0;
var workspace = "";
for (const c of /^\((.+)\)$/.exec(TypeName)[1]) {
switch (c) {
case '(':
case '<':
depth++;
break;
case ')':
case '>':
depth--;
break;
case ',':
if (depth == 0) {
this.mode = "tuple";
types.push(workspace);
workspace = "";
continue;
}
break;
case '|':
if (depth == 0) {
this.mode = "union";
types.push(workspace);
workspace = "";
continue;
}
break;
}
workspace += c;
}
if (workspace) types.push(workspace);
this.template = types.map(t => t.trim()).map(t => new Type(t, Params));
}
//Template
else if (/^[^<]+<.+>$/.test(TypeName)) {
this.typename = /^([^<]+)</.exec(TypeName)[1];
this.mode = "templated";
const types = [];
var depth = 0;
var workspace = "";
for (const c of /^[^<]+<(.+)>$/.exec(TypeName)[1]) {
switch (c) {
case '(':
case '<':
depth++;
break;
case ')':
case '>':
depth--;
break;
case ',':
if (depth == 0) {
types.push(workspace);
workspace = "";
continue;
}
break;
}
workspace += c;
}
if (workspace) types.push(workspace);
this.template = types.map(t => t.trim()).map(t => new Type(t, Params));
Type.all[this.typename] = () => new Type(TypeName, Array.from(Params));
}
//Standard Type, or Template Param
else if (/^[^()|<>,]+$/.test(TypeName)) {
if (!Params.includes(TypeName)) {
this.typename = TypeName;
this.mode = (TypeName == 'any') ? "any" : "standard";
if (this.mode == "standard") Type.all[this.typename] = () => new Type(TypeName);
} else {
this.mode = "param";
this.typename = TypeName;
this.template = Type.any;
}
}
}
Type.prototype.toString = function(info=false) {
switch (this.mode) {
case "tuple":
return `(${this.template.join(',')})`;
case "union":
return `(${this.template.join('|')})`;
case "templated":
return `${this.typename}<${this.template.join(',')}>`;
case "any":
case "standard":
return this.typename;
case "param":
return info ? `${this.typename}: ${this.template}` : this.template.toString();
}
}
Type.prototype._resolve = function(key, val) {
if (this.mode != "param") {
if (val === undefined && this.template?.length === 1) {
val = key;
key = 0;
}
if (this.template?.length) this.template = this.template.map(t => t.resolve(key, val));
} else {
if (this.typename == key) this.template = new Type(val)
}
}
Type.prototype.resolve = function(key, val) {
const t = this.copy();
t._resolve(key, val);
return t;
}
Type.prototype.copy = function() {
if (this.mode != "param") {
const t = new Type(this.toString());
for (const k in this.template)
t.template[k] = this.template[k].copy();
return t;
} else {
const t = new Type(this.typename, [this.typename]);
t._resolve(this.typename, this.toString())
return t;
}
}
Type.prototype.getClasses = function() {
if (this.mode == "param") return this.template.getClasses();
const ret = new Set();
switch (this.mode) {
case 'standard':
switch (this.typename) {
case 'bool':
case 'int':
case 'float':
case 'string':
ret.add(this.typename);
break;
default:
ret.add('special');
break;
}
break;
case 'templated':
if (this.template.length == 1) {
if (this.typename == "List") ret.add("list");
this.template[0].getClasses().forEach(ret.add.bind(ret));
} else {
ret.add('any');
}
break;
case 'union':
case 'tuple':
case 'any':
ret.add('any');
break;
}
return Array.from(ret);
}
Type.prototype.getParams = function(set=new Set()) {
switch(this.mode) {
case 'param':
set.add(this.typename);
this.template.getParams(set);
break;
case 'templated':
case 'union':
case 'tuple':
this.template.map(t => t.getParams(set));
break;
}
return set;
}
//just makes my life that much easier when writing code. Type.any is nicer than new Type('any')
Type.typeDef = (function(typename, name, params=[]) {
Object.defineProperty(this, typename, {
get: () => new Type(name, params)
})
}).bind(Type)
Type.QuickTD = (function(name, params=[]) {
Object.defineProperty(this, (new Type(name, params)).typename, {
get: () => new Type(name, params)
})
}).bind(Type)
Type.QuickTD("any");
Type.QuickTD("int");
Type.QuickTD("float");
Type.QuickTD("string");
Type.QuickTD("bool");
Type.QuickTD("Vector3");
Type.typeDef("list", "List<T>", ["T"]);
function intersect(type1, type2) {
const T1 = new Type(type1.toString());
const T2 = new Type(type2.toString());
if(T1 == T2) return T1;
if (T1.mode == "any") return T2;
if (T2.mode == "any") return T1;
if (T1.mode == T2.mode) {
switch (T1.mode) {
case "standard":
return (String(T1) == String(T2)) ? T1 : null;
case "templated":
case "tuple":
if (
T1.typename != T2.typename ||
T1.template.length != T2.template.length
) return null;
var t = T1.copy();
for (const k in T1.template)
t.template[k] = intersect(T1.template[k], T2.template[k]);
if (t.template.includes(null))
return null;
else return t;
case "union":
const rettempl = Array.from(
new Set(
T1.template.map(
t => T2.template
.map(t2 => intersect(t,t2))
.filter(t => t ?? false))
.flat().map(String)
)).map(n => new Type(n));
if (rettempl.length > 1) {
const ret = T1.copy();
ret.template = rettempl;
return ret;
}
else if (rettempl.length) return rettempl[0];
return null
}
} else if ([T1.mode,T2.mode].includes("union")) {
const UT = (T1.mode == "union") ? T1 : T2;
const ST = (T1.mode == "union") ? T2 : T1;
const isect = Array.from(
new Set(
UT.template
.map(t => intersect(ST, t))
.filter(t => t ?? false)
.map(String)
)).map(n => new Type(n));
if (isect.length > 1) {
const ret = UT.copy();
ret.template = isect;
return ret;
}
else if (isect.length) return isect[0];
}
return null;
}
function walkDownParams(t, tres) {
let ret = {};
if (t.mode == "param") {
ret = {[t.typename]: tres};
}
else if (
t.mode == tres.mode &&
t.typename == tres.typename &&
t.template?.length == tres.template?.length
) {
for (const p in t.template)
ret = {...ret, ...walkDownParams(t.template[p], tres.template[p])};
}
return ret;
}
function Port(name, type, parent, input) {
//basics
this.parent = parent;
this.name = name;
//what have we here
this.base = (type == "exec") ? "exec" : parent.passed.reduce((t, k) => t.resolve(k, parent.params[k]), new Type(type, parent.passed));
this.resolve()
//what does it do
this.conn = input ? null : new Set();
this.side = input ? "in" : "out";
//enter your mommy's SSN
this.id = crypto.randomUUID();
Port.ports.set(this.id, this);
//on TV!
this._el = {};
Object.defineProperty(this, 'el', {get: () => this._el, set: val => {this._el.id = ''; this._el = val; val.id = this.id}})
}
Port.ports = new Map();
Port.prototype.resolve = function () {
this.type =
(this.base == "exec") ?
"exec" :
this.parent.passed.reduce((t, k) => t.resolve(k, this.parent.params[k]), new Type(this.base, this.parent.passed));
}
Port.prototype.getIDStr = function () {
return `${this.parent.id}/${this.side}/${this.id}`;
}
Port.getByID = function(str) {
const [id, side, id2] = str.split('/');
if (
id !== undefined &&
side !== undefined &&
id2 !== undefined
) {
const side = Chip.chips.get(id)[side+'s'];
return side.filter(p => p.id == id2)[0];
}
}
Port.prototype.remove = function(cb = false) {
if (this.side == "in")
unwireDataIn(this);
else
unwireDataOut(this);
if (cb) this.parent[this.side + 's'] = this.parent[this.side + 's'].filter(p => p != this);
Port.ports.delete(this.id);
}
function paramRecurse(ports, portCB, cancelCB=()=>false, seenchips=new Set()) {
for (const p of ports) {
if (cancelCB(p)) return;
const c = p.parent;
if (seenchips.has(c)) continue;
portCB(p);
seenchips.add(c);
const params = p.base.getParams();
if (!params.size) continue;
paramRecurse([...c.ins, ...c.outs].filter(fp => {
for (const par of fp.base.getParams())
if (params.has(par)) return true;
return false;
}).flatMap(p => (p.conn instanceof Set) ? [...p.conn] : p.conn).filter(p => p), portCB, cancelCB, seenchips);
}
return seenchips;
}
//TODO:
//Please someone other than me go in and optimize this absolute shitshow.
//There are better ways than this to write code but I am too tired to write them.
//Of the things for future me, or some kind soul:
// refactor this to only do one pass of the graph.
// cache things like getParams() and toString() on first call.
// do something clever to progressively update the types for everything. maybe make related wires share a reference to an object and mutate that.
//Sorry in advance to all people on potato computers trying to run this slow piece of junk
//future me here, tbh its not too bad and modern js zooms. my perf tests have put me at ease.
//it ciuld be faster, but it should be good for normal use
function regenTypes(ports, base=Type.any) {
if (ports instanceof Port) ports = [ports];
for (const port of ports)
if (!(port instanceof Port)) throw new TypeError("Can only regen Ports!");
//step 1: recursively search for any issues with typing.
// 1A: get all connected chips - Done, see paramRecurse
// 1B: only get chips that share a chain of params. - Done, see paramRecurse
// 1C: aggregate types as you go on. - Done
// 1D: fail if null - Done
const basechips = [...ports];
let finaltype = base;
const sc = paramRecurse(
basechips,
p => {finaltype = intersect(finaltype, p.base)},
() => finaltype==null
);
if (finaltype == null) return false;
for (const chip of sc)
chip.root.append(`seen! ${finaltype}`);
//step 2: recursively set all relevant type params, regen UI
paramRecurse(basechips, p => {
let type = finaltype.copy();
let sect = walkDownParams(p.base, type);
console.log(sect);
p.parent.setParams(sect);
p.parent.regenChip();
});
//step 3: cleanup, clear markers
return finaltype;
}
function wireData(port1, port2) {
//step 1: check things
// 1.0: both data?
if (!(port1.base instanceof Type && port2.base instanceof Type)) return false;
// 1.1: same side?
if (port1.side == port2.side) return false;
// 1.2: already connected?
const [input, output] = port1.side == "in" ? [port1, port2] : [port2, port1];
if (output.conn.has(input)) return true;
// 1.3: disconnect input
let oldconn;
if (input.conn != null) {
oldconn = input.conn;
input.conn = null;
oldconn.conn.delete(input)
}
//step 2: do the actual stuff
// 2A: they're identical
if (input.type.toString() == output.type.toString()) {
// 2A.1: make the connections
output.conn.add(input);
input.conn = output;
Chip.regenconnections();
return true;
}
// 2B: they're different, one+ has params
if (output.base.getParams().size + input.base.getParams().size) {
// 2B.1: make the connections temporarily
output.conn.add(input);
input.conn = output;
// 2B.2: try recursing down them
const type = regenTypes(input);
// 2B.3: rollback if necessarry
if(!type) {
unwireDataIn(input);
if (oldconn) wireData(input, oldconn);
}
Chip.regenconnections();
return true;
}
// 2C: they're different
// 2C.1: rollback everything
if (oldconn) wireData(input, oldconn);
return false;
//step 3: final bits
// 2.1: regen connections
Chip.regenconnections();
}
function unwireDataIn(input, regen=true) {
const output = input.conn;
if (output == null) return null;
input.conn = null;
output.conn.delete(input);
if(regen) {
const regens = [input, output].filter(p => p.base.getParams().size);
regens.map(r => regenTypes(r));
}
Chip.regenconnections();
}
function unwireDataOut(output, regen=true) {
const inputs = [...output.conn];
if (inputs.length == 0) return [];
output.conn.clear();
for (const i of inputs)
i.conn = null;
if(regen) {
const regens = [...inputs, output].filter(p => p.base.getParams().size);
regens.map(r => regenTypes(r));
}
Chip.regenconnections();
}
function Chip(Entry, NodeDesc, el) {
this.id = crypto.randomUUID();
this.root = el;
const cur = NodeDesc;
const params = Object.fromEntries(Object.entries(cur.ReadonlyTypeParams).map(([n,t]) => [n, new Type(t)]));
const passed = Object.keys(params)
this.chip = Entry;
this.nd = cur;
this.defparams = {...params};
this.params = {...params};
this.passed = Object.keys(params);
const portToType = (input, idx) => ((port) => new Port(port.Name, port.ReadonlyType, this, input, idx));
let i = 0;
this.ins = cur.Inputs.map(portToType(true, i++));
i = 0;
this.outs = cur.Outputs.map(portToType(false, i++));
this.regenChip();
//this.regenUI();
Chip.chips.set(this.id, this);
}
Chip.chips = new Map(); // list that chips register themselves into when constructed.
Chip.connections = new Set();
Chip.regenconnections = function() {
Chip.connections.clear();
for (const chip of Chip.chips.values()) {
for (const input of chip.ins)
if (input.type != "exec" && input.conn)
Chip.connections.add(`${input.conn.id} => ${input.id}`);
for (const output of chip.outs)
if (output.type == "exec" && output.conn)
Chip.connections.add(`${output.id} => ${output.conn.id}`);
}
}
Chip.prototype.setParams = function (params) {
for (const p of Object.keys(params).filter(p => this.passed.includes(p)))
this.params[p] = params[p];
for (const port of [...this.ins, ...this.outs])
port.resolve();
}
Chip.prototype.regenChip = function () {
this.root.querySelector(".chip")?.remove();
const chip = newEl('div', 'chip');
chip.id = this.id;
const header = newEl('div', 'chipheader');
header.innerText = this.nd.Name;
const input = newEl('div', 'input');
const output = newEl('div', 'output');
chip.append(header, input, output);
const genPort = (port) => {
let classes,name;
if (port.type != "exec") {
classes = port.type.getClasses();
name = port.type.toString();
} else {
classes = ["exec"];
name = "exec";
}
const ret = newEl('div', '');
ret.id = port.id
ret.innerHTML = port.name + " ";
classes.forEach(p => ret.classList.add(p))
port.el = ret;
const tooltip = newEl('div', 'type');
tooltip.innerText = name;
return [ret, tooltip];
}
input.append(...this.ins.flatMap(genPort));
output.append(...this.outs.flatMap(genPort));
this.root.prepend(chip);
}
Chip.prototype.remove = function() {
for (const p of [...this.ins, ...this.outs])
p.remove();
Chip.chips.delete(this.id);
this.root.remove();
//regenconnections
}
//chips.js reimplemented with the above
let root = document.documentElement;
if (window.location.pathname != '/grapher/') {
root.addEventListener("mousemove", e => {
root.style.setProperty('--mouse-x', e.clientX + "px");
root.style.setProperty('--mouse-y', e.clientY + "px");
});
}
const portColors = {
float: '#186adc',
int: '#0f6522',
exec: '#f55b18',
string: '#794284',
bool: '#ea2e50',
any: '#f6eee8',
special: '#f4c61e'
}
const typeRegex = /(?:^|(?<=<))(?:int|float|bool|string|exec)(?:$|(?=>))/;
const unionRegex = /^T\d*$|(?<=List<)T\d*(?=>)/;
function computeType(tn, TypeParams, to) {
if (tn != "exec") {
const t = Object.entries({...TypeParams, ...to}).reduce((t,i) => t.resolve(...i), new Type(tn, Object.keys(TypeParams)));
return {typeclass: t.getClasses(), type: t.toString()};
} else {
return {typeclass: ['exec'], type: 'exec'};
}
}
//keeping this the same for now
function generateChipHTML(NodeDescs, typeoverride = undefined) {
for (let cur of NodeDescs) {
let ins = cur.Inputs;
let outs = cur.Outputs;
const root = newEl('div', 'chip');
const header = newEl('div', 'chipheader');
header.innerText = cur.Name;
const input = newEl('div', 'input');
const output = newEl('div', 'output');
root.append(header, input, output);
for (const inp of ins) {
//work out the type
let {typeclass, type} = computeType(inp.ReadonlyType, cur.ReadonlyTypeParams, typeoverride);
const port = newEl('div', '');
port.innerHTML = inp.Name + " ";
typeclass.map(port.classList.add.bind(port.classList));
const tooltip = newEl('div', 'type');
tooltip.innerText = type;
input.append(port, tooltip);
}
for (const out of outs) {
//work out the type
let {typeclass, type} = computeType(out.ReadonlyType, cur.ReadonlyTypeParams, typeoverride);
const port = newEl('div', '');
port.innerHTML = out.Name + " ";
typeclass.map(port.classList.add.bind(port.classList));
const tooltip = newEl('div', 'type');
tooltip.innerText = type;
output.append(port, tooltip);
}
return root;
}
}
function ListAllTypes(Nodes) {
return Object.keys(Type.all);
}