ref: 81d1e4e01d7001e6a0107792e6167136787295d6
dir: /compile.js/
import {flatten} from "./model.js" import * as core from "./core.js" import {BooleanParameter, TextParameter, Define} from "./core.js" let map = new Map(Object.entries({ // stack blocks SetVariable: (ctx, a, block) => `${variableName(block.names[0])}.value = ${ctx.cc(a)}`, ChangeVariable: (ctx, a, block) => `${variableName(block.names[0])}.value = (+${variableName(block.names[0])}.value || 0) + ${ctx.ccNumber(a)}`, WhenFlagClicked: (ctx, before, block) => `onDispatch(${block.id}, event => event === "start" ? start${block.id} : undefined)\nfunction * start${block.id}(tick)\n${ctx.ccStack(block, {before: [before, `let id = ${block.id}`].filter(Boolean).join("\n")})}`, If: (ctx, condition, block) => `if ${ctx.cc(condition)} ${ctx.ccStack(block)}${block.complement ? "\nelse " + ctx.ccStack(block.complement) : ""}`, Forever: (ctx, block) => `for (;;) ${ctx.ccStack(block)}`, RepeatUntil: (ctx, condition, block) => `while (!${ctx.cc(condition)}) ${ctx.ccStack(block)}`, WaitUntil: (ctx, condition) => `while (!${ctx.cc(condition)}) ${ctx.ccTick() || "{ }"}`, Repeat: (ctx, count, block) => `for (let i = ${ctx.ccNumber(count)} - 0.5 ; i > 0 ; i--) ${ctx.ccStack(block)}`, Append: (ctx, value, block) => `${listName(block.names[0])}.push${ctx.cc(value)}`, Clear: (_ctx, block) => `${listName(block.names[0])}.length = 0`, Delete: (ctx, index, block) => `{ let i = ${ctx.ccInteger(index)}, l = ${listName(block.names[0])} ; if (i > 0 && i <= l.length) l.splice(i - 1, 1) }`, Insert: (ctx, index, value, block) => `{ let i = ${ctx.ccInteger(index)}, l = ${listName(block.names[0])} ; if (i > 0 && i <= l.length + 1) l.splice(i - 1, 0, ${ctx.cc(value)}) }`, Replace: (ctx, index, value, block) => `{ let i = ${ctx.ccInteger(index)}, l = ${listName(block.names[0])} ; if (i > 0 && i <= l.length) l[i - 1] = ${ctx.cc(value)} }`, Stop: () => "return", // reporters/booleans Variable: (_ctx, block) => `${variableName(block.names[0])}.value`, List: (_ctx, block) => `${listName(block.names[0])}.join(" ")`, Add: (ctx, a, b) => `${ctx.ccNumber(a)} + ${ctx.ccNumber(b)}`, Subtract: (ctx, a, b) => `${ctx.ccNumber(a)} - ${ctx.ccNumber(b)}`, Multiply: (ctx, a, b) => `${ctx.ccNumber(a)} * ${ctx.ccNumber(b)}`, Divide: (ctx, a, b) => `${ctx.ccNumber(a)} / ${ctx.ccNumber(b)}`, Modulo: (ctx, a, b) => `mod(${ctx.ccNumber(a)}, ${ctx.ccNumber(b)})`, LessThan: (ctx, a, b) => `compare(${ctx.cc(a)}, ${ctx.cc(b)}) < 0`, GreaterThan: (ctx, a, b) => `compare(${ctx.cc(a)}, ${ctx.cc(b)}) > 0`, Equals: (ctx, a, b) => `compare(${ctx.cc(a)}, ${ctx.cc(b)}) === 0`, And: (ctx, a, b) => `!!(${ctx.cc(a)} && ${ctx.cc(b)})`, Or: (ctx, a, b) => `!!(${ctx.cc(a)} || ${ctx.cc(b)})`, Not: (ctx, a) => `!${ctx.cc(a)}`, Join: (ctx, a, b) => `${ctx.ccString(a)} + ${ctx.ccString(b)}`, Index: (ctx, i, block) => `fallback(${listName(block.names[0])}[${ctx.ccInteger(i)} - 1], "")`, Find: (ctx, a, block) => `${listName(block.names[0])}.findIndex(v => compare(v, ${ctx.cc(a)}) === 0) + 1`, Length: (_ctx, block) => `${listName(block.names[0])}.length`, Contains: (ctx, a, block) => `${listName(block.names[0])}.some(v => compare(v, ${ctx.cc(a)}) === 0)`, TextIndex: (ctx, a, i) => `fallback(${ctx.ccString(a)}[${ctx.ccInteger(i)} - 1], "")`, TextLength: (ctx, a) => `${ctx.ccString(a)}.length`, TextContains: (ctx, a, b) => `${ctx.ccLower(a)}.includes${ctx.ccLower(b)}`, Define: (ctx, before0, block) => { if (block.atomic && ctx.tick) ctx = ctx.with({tick: false}) let names = customBlockInputNames(block.names[0]) let before = [] if (ctx.debug) before.push(`let debug = id => debug0(id, {parameters: new Map([${names.map(n => `[${JSON.stringify(n[0])}, ${n[1]}]`).join(", ")}])})`) if (before0) before.push(before0) return `function * ${customBlockName(block.names[0])}(${["id, tick", ...names.map(n => n[1])].join(", ")})\n${ctx.ccStack(block, {before: before.join("\n")})}` }, }).map(([name, fn]) => [core[name].type, fn])) function customName(name) { let result = "" for (let part of name) { if (typeof part !== "string") { if (part.type === "boolean") result += "$b$" else result += "$s$" continue } for (let ch of part) { if (ch === " ") { result += "_" continue } if (/^[a-zA-Z0-9]$/.test(ch)) { result += ch continue } result += `$u${ch.codePointAt().toString(16)}$` } } return result } function parameterName(value, type) { return "parameter_" + type + "_" + customName([value]) } function customBlockName(name) { return "procedure_" + customName(name) } function variableName(name) { return "variable_" + customName([name]) } function listName(name) { return "list_" + customName([name]) } function customBlockInputNames(name) { let names = [] for (let part of name) { if (typeof part === "string") continue names.push([part.name, parameterName(part.name, part.type)]) } return names } function compileReporter(block, ctx) { if (block.type.shape === "slot") return JSON.stringify(block.value) if (block.type === TextParameter.type || block.type === BooleanParameter.type) { let type = block.type === BooleanParameter.type ? "boolean" : "text" let block1 = block while (block1 && block1.type !== Define.type) block1 = block1.parent if (block1?.names[0].some(part => typeof part !== "string" && part.name === block.names[0] && part.type === type)) { return parameterName(block.names[0], type) } if (block.type === BooleanParameter.type) return "false" return `""` } let fn = map.get(block.type) if (fn) return fn(ctx, ...block.inputs, block) let name = ctx.extensions?.get(block.type) if (!name) { let type1 = block.type.output ?? "unknown" if (["number", "integer", "natural", "positive"].includes(type1)) return "0" if (type1 === "boolean") return "false" return `""` } let references = block.names?.map((name, i) => block.type.references[i] === "list" ? listName(name) : variableName(name)) ?? [] let inputs = [...references, ...block.inputs.map(block => compileReporter(block, ctx))] let w = block.type.async ? "yield " : "" return w + `${name}(${inputs.join(", ")})` } function compileStackBlock(block, ctx) { let inputs = block.inputs if (block.type.category === "custom" && block.type.shape === undefined && !block.type.hat) { let types = block.names[0].map(part => typeof part !== "string").map(part => part.type) return `yield * ${customBlockName(block.names[0])}(${[ctx.tick ? "id, tick" : "id, () => { }", ...inputs.map((block, i) => types[i] === "boolean" ? ctx.ccBoolean(block) : ctx.cc(block))].join(", ")})` } let before = "" if (ctx.debug) before = `yield debug(${block.id})` if (!map.has(block.type) && block.type.hat) { let name = ctx.extensions?.get(block.type) if (!name) return "" inputs = inputs.map(block => compileReporter(block, ctx)) return `onDispatch(${block.id}, event => ${name}(${["event", ...inputs].join(", ")}) ? block${block.id} : undefined)\nfunction * block${block.id}(tick)\n${compileStack(block, {before: [before, `let id = ${block.id}`].filter(Boolean).join("\n")}, ctx)}\n` } if (block.type.hat) { inputs = inputs.slice() inputs.push(before) before = "" } if (before) before += "\n" let fn = map.get(block.type) if (fn) return before + fn(ctx, ...inputs, block) let name = ctx.extensions?.get(block.type) if (!name) return "" inputs = inputs.map(block => compileReporter(block, ctx)) let after = block.type.shape === "cap" ? "\nreturn" : "" let w = block.type.async ? "yield " : "" return before + w + `${name}(${[...inputs, "{id}"].join(", ")})` + after } function compileConverting(block, convert, compile, type, ctx) { if (block.type.shape === "slot") return "(" + JSON.stringify(convert(block.value)) + ")" let type1 = block.type.output ?? "unknown" if (type1 === type || type === "number" && ["integer", "natural", "positive"].includes(type1) || ["integer", "positive"].includes(type) && type1 === "natural") { return "(" + compileReporter(block, ctx) + ")" } return "(" + compile("(" + compileReporter(block, ctx) + ")") + ")" } function compileStack(block, {before, after} = {}, ctx) { let result = [...before ? [before] : [], ...block.stack?.map(block => compileStackBlock(block, ctx)).filter(Boolean) ?? [], ...after ? [after] : []] if (ctx.debug) result.push(`yield debug(${block.id})`) if (block.type.loop) result.push(ctx.ccTick()) result = result.filter(Boolean) if (result.length === 0) return "{\n}" return `{\n${result.join("\n").replace(/^/mg, "\t")}\n}` } export function compileForDispatcher(scripts, {variables = {}, lists = {}, debug, dispatcher: dispatcher0 = dispatcher} = {}) { let extensions = new Map() let every = scripts.flatMap(flatten) let names = [] let values = [] let id = 1 for (let block of every) { if (block.type.category === "custom") continue if (map.has(block.type)) continue if (extensions.has(block.type)) continue extensions.set(block.type, `extension_${id++}`) } function onDispatch(id, fn) { dispatcher0.onDispatch(id, event => { let fn1 = fn(event) if (!fn1) return return fn2 async function fn2(tick, info) { for (let value of fn1(tick)) { if (typeof value?.then === "function") { if (value.sync) await info.pause(value) else await value } if (info.done) break } } }) } let stuff = { fallback: (a, b) => a ?? b, debug, debug0: debug, mod, compare, onDispatch, } for (let [name, value] of Object.entries(stuff)) { names.push(name) values.push(value) } for (let name of variables.keys()) { names.push(variableName(name)) values.push(variables.get(name)) } for (let name of lists.keys()) { names.push(listName(name)) values.push(lists.get(name)) } for (let [type, name] of extensions) { names.push(name) values.push(type.run ?? (() => { })) } let ctx = new Context({extensions, debug: Boolean(debug), tick: true}) let compiled = scripts.filter(block => block.type.hat).map(block => compileStackBlock(block, ctx)).filter(Boolean).join("\n\n").replace(/^/mg, "\t") Function(...names, compiled)(...values) } class Context { constructor(properties) { Object.assign(this, properties) } with(properties) { return new Context({...this, ...properties}) } cc(block) { return "(" + compileReporter(block, this) + ")" } ccStack(block, extra = {}) { return compileStack(block, extra, this) } ccString(block) { return compileConverting(block, String, n => `"" + ${n}`, "text", this) } ccLower(block) { return compileConverting(block, n => String(n).toLowerCase(), n => `("" + ${n}).toLowerCase()`, "-", this) } ccNumber(block) { return compileConverting(block, toNumber, n => `+${n} || 0`, "number", this) } ccInteger(block) { return compileConverting(block, toInteger, n => `Math.floor${n} || 0`, "-", this) } ccBoolean(block) { return compileConverting(block, toBoolean, n => `!!${n}`, "boolean", this) } ccTick() { if (this.tick) return "yield tick()" } } export function toBoolean(v) { return Boolean(v) } export function toString(v) { return String(v) } export function toNumber(n) { return Number(n) || 0 } export function toInteger(n) { return Math.floor(toNumber(n)) } function mod(a, b) { return (a % b + b) % b } function compare(a, b) { let n = a - b if (n === n && a !== "" && b !== "") return n a = String(a).toLowerCase() b = String(b).toLowerCase() if (a < b) return -1 if (a > b) return 1 return 0 } export class Ticker { #done = true #running = false #listeners = [] #frequency constructor(frequency) { this.#frequency = frequency ?? 30 this.start() } start() { this.#start() } pause() { this.#running = false } tick() { return new Promise(resolve => this.#listeners.push(resolve)) } async #start() { if (this.#running || !this.#done) return this.#done = false this.#running = true let t0 = performance.now() while (this.#running) { for (let fn of this.#listeners.splice(0)) fn() t0 += 1000 / this.#frequency let t1 = performance.now() if (t0 < t1) t0 = t1 await new Promise(resolve => setTimeout(resolve, t0 - t1)) } this.#done = true } } export class Dispatcher { #tick0 = () => ticker.tick() #pause #fns = new Map() #fns2 = new Map() constructor(tick) { if (tick) this.#tick0 = tick } onDispatch(id, fn) { this.#fns.set(id, fn) } async start(ids) { await this.dispatch("start", ids) } stop(ids) { for (let id of ids ?? [...this.#fns2.keys()]) { this.#fns2.get(id)?.() } } remove(ids) { for (let id of ids ?? [...this.#fns.keys()]) { this.#fns.delete(id) } } all() { return [...this.#fns.keys()] } dispatch(event, ids) { return Promise.all([...this.#fns].map(([id, fn]) => { if (ids && !ids.includes(id)) return let fn1 = fn(event) if (!fn1) return this.#fns2.get(id)?.() this.#fns2.set(id, () => info.done = true) let info = {done: false, pause: promise => this.#pause = promise.then(() => this.#pause = undefined)} return Promise.resolve().then(() => this.#tick()).then(() => fn1(() => this.#tick(), info)) })) } async #tick() { await this.#pause return this.#tick0() } } export let ticker = new Ticker() export let dispatcher = new Dispatcher()