//Gods-awful type-checking logic. //TypeName: string, Params: Array 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]) { 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"]); 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); }