ref: 86a1bffad7063d1f51dfe44c2126bbdf8b7fe5c8
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()