shithub: scrax

ref: 86a1bffad7063d1f51dfe44c2126bbdf8b7fe5c8
dir: /compile.js/

View raw version
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()