rkgk/static/haku.js

477 lines
14 KiB
JavaScript

let panicImpl;
let logImpl, log2Impl;
let canvasBeginImpl,
canvasLineImpl,
canvasRectangleImpl,
canvasCircleImpl,
canvasFillImpl,
canvasStrokeImpl;
function allocCheck(p) {
if (p == 0) throw new Error("out of memory");
return p;
}
function makeLogFunction(level) {
return (length, pMessage) => {
logImpl(level, length, pMessage);
};
}
function makeLogFunction2(level) {
return (pScope, scopeLen, pMsg, len) => {
log2Impl(level, pScope, scopeLen, pMsg, len);
};
}
// NOTE: It may seem tempting to share memory between the two modules, but that's *impossible*.
// This is because Wasm binaries are not position-independent; addresses of the stack and the heap
// are hardcoded.
let [hakuWasm, haku2Wasm] = await Promise.all([
WebAssembly.instantiateStreaming(fetch(HAKU_WASM_PATH), {
env: {
panic(length, pMessage) {
panicImpl(length, pMessage);
},
trace: makeLogFunction("trace"),
debug: makeLogFunction("debug"),
info: makeLogFunction("info"),
warn: makeLogFunction("warn"),
error: makeLogFunction("error"),
},
}),
WebAssembly.instantiateStreaming(fetch(HAKU2_WASM_PATH), {
env: {
__haku2_log_err: makeLogFunction2("error"),
__haku2_log_warn: makeLogFunction2("warn"),
__haku2_log_info: makeLogFunction2("info"),
__haku2_log_debug: makeLogFunction2("debug"),
__haku2_canvas_begin: (c) => canvasBeginImpl(c),
__haku2_canvas_line: (c, x1, y1, x2, y2) => canvasLineImpl(c, x1, y1, x2, y2),
__haku2_canvas_rectangle: (c, x, y, width, height) =>
canvasRectangleImpl(c, x, y, width, height),
__haku2_canvas_circle: (c, x, y, r) => canvasCircleImpl(c, x, y, r),
__haku2_canvas_fill: (c, r, g, b, a) => canvasFillImpl(c, r, g, b, a),
__haku2_canvas_stroke: (c, r, g, b, a, thickness) =>
canvasStrokeImpl(c, r, g, b, a, thickness),
},
}),
]);
let memory = hakuWasm.instance.exports.memory;
let w = hakuWasm.instance.exports;
let memory2 = haku2Wasm.instance.exports.memory;
let w2 = haku2Wasm.instance.exports;
let textEncoder = new TextEncoder();
function allocString(string) {
let size = string.length * 3;
let align = 1;
let pString = allocCheck(w.haku_alloc(size, align));
let buffer = new Uint8Array(memory.buffer, pString, size);
let result = textEncoder.encodeInto(string, buffer);
return {
ptr: pString,
length: result.written,
size,
align,
};
}
function freeString(alloc) {
w.haku_free(alloc.ptr, alloc.size, alloc.align);
}
let textDecoder = new TextDecoder();
function readString(memory, size, pString) {
let buffer = new Uint8Array(memory.buffer, pString, size);
return textDecoder.decode(buffer);
}
function readCString(pCString) {
let memoryBuffer = new Uint8Array(memory.buffer);
let pCursor = pCString;
while (memoryBuffer[pCursor] != 0 && memoryBuffer[pCursor] != null) {
pCursor++;
}
let size = pCursor - pCString;
return readString(memory, size, pCString);
}
function dup1to2(pHaku1, size, align) {
if (size <= 0) {
return {
ptr: 0,
length: 0,
size: 0,
align: 1,
};
}
let pHaku2 = allocCheck(w2.haku2_alloc(size, align));
let src = new Uint8Array(memory.buffer, pHaku1, size);
let dst = new Uint8Array(memory2.buffer, pHaku2, size);
dst.set(src);
return {
ptr: pHaku2,
length: size,
size,
align,
};
}
function freeString2(alloc) {
if (alloc.ptr == 0) return;
w2.haku2_free(alloc.ptr, alloc.size, alloc.align);
}
class Panic extends Error {
name = "Panic";
}
panicImpl = (length, pMessage) => {
throw new Panic(readString(memory, length, pMessage));
};
logImpl = (level, length, pMessage) => {
console[level]("h1:", readString(memory, length, pMessage));
};
log2Impl = (level, pScope, scopeLen, pMsg, len) => {
console[level](
"h2:",
`${readString(memory2, scopeLen, pScope)}: ${readString(memory2, len, pMsg)}`,
);
};
canvasBeginImpl = w.haku_pixmap_begin;
canvasLineImpl = w.haku_pixmap_line;
canvasRectangleImpl = w.haku_pixmap_rectangle;
canvasCircleImpl = w.haku_pixmap_circle;
canvasFillImpl = w.haku_pixmap_fill;
canvasStrokeImpl = w.haku_pixmap_stroke;
w.haku_init_logging();
export class Pixmap {
#pPixmap = 0;
constructor(width, height) {
this.#pPixmap = allocCheck(w.haku_pixmap_new(width, height));
this.width = width;
this.height = height;
}
destroy() {
w.haku_pixmap_destroy(this.#pPixmap);
}
clear(r, g, b, a) {
w.haku_pixmap_clear(this.#pPixmap, r, g, b, a);
}
get ptr() {
return this.#pPixmap;
}
getArrayBuffer() {
return new Uint8ClampedArray(
memory.buffer,
w.haku_pixmap_data(this.#pPixmap),
this.width * this.height * 4,
);
}
getImageData() {
return new ImageData(this.getArrayBuffer(), this.width, this.height);
}
}
export const ContKind = {
Scribble: 0,
Dotter: 1,
};
export class Haku {
#pInstance = 0;
#pLimits2 = 0;
#pScratch2 = 0;
#pDefs2 = 0;
#pVm2 = 0;
#bytecode2 = null;
#localCount = 0;
#fuel = 0;
#renderMaxDepth = 0;
constructor(limits) {
console.groupCollapsed("construct Haku");
{
let pLimits = allocCheck(w.haku_limits_new());
w.haku_limits_set_max_source_code_len(pLimits, limits.max_source_code_len);
w.haku_limits_set_max_chunks(pLimits, limits.max_chunks);
w.haku_limits_set_max_defs(pLimits, limits.max_defs);
w.haku_limits_set_max_tags(pLimits, limits.max_tags);
w.haku_limits_set_max_tokens(pLimits, limits.max_tokens);
w.haku_limits_set_max_parser_events(pLimits, limits.max_parser_events);
w.haku_limits_set_ast_capacity(pLimits, limits.ast_capacity);
w.haku_limits_set_chunk_capacity(pLimits, limits.chunk_capacity);
this.#pInstance = allocCheck(w.haku_instance_new(pLimits));
w.haku_limits_destroy(pLimits);
}
console.groupEnd();
// NOTE(v2): VM is not constructed until there is a brush ready
this.#pLimits2 = allocCheck(w2.haku2_limits_new());
w2.haku2_limits_set_stack_capacity(this.#pLimits2, limits.stack_capacity);
w2.haku2_limits_set_call_stack_capacity(this.#pLimits2, limits.call_stack_capacity);
this.#fuel = limits.fuel;
this.#renderMaxDepth = limits.render_max_depth;
this.#pScratch2 = allocCheck(w2.haku2_scratch_new(limits.memory));
}
destroy() {
w.haku_instance_destroy(this.#pInstance);
w2.haku2_scratch_destroy(this.#pScratch2);
w2.haku2_vm_destroy(this.#pVm2);
w2.haku2_defs_destroy(this.#pDefs2);
w2.haku2_limits_destroy(this.#pLimits2);
freeString2(this.#bytecode2);
}
setBrush(code) {
w.haku_reset(this.#pInstance);
if (this.#pVm2 != 0) w2.haku2_vm_destroy(this.#pVm2);
if (this.#pDefs2 != 0) w2.haku2_defs_destroy(this.#pDefs2);
if (this.#bytecode2 != null) freeString2(this.#bytecode2);
this.#pVm2 = 0;
this.#pDefs2 = 0;
this.#bytecode2 = null;
let brushCode = allocString(code);
let statusCode = w.haku_compile_brush(this.#pInstance, brushCode.length, brushCode.ptr);
freeString(brushCode);
if (!w.haku_is_ok(statusCode)) {
if (w.haku_is_diagnostics_emitted(statusCode)) {
let diagnostics = [];
for (let i = 0; i < w.haku_num_diagnostics(this.#pInstance); ++i) {
diagnostics.push({
start: w.haku_diagnostic_start(this.#pInstance, i),
end: w.haku_diagnostic_end(this.#pInstance, i),
message: readString(
memory,
w.haku_diagnostic_message_len(this.#pInstance, i),
w.haku_diagnostic_message(this.#pInstance, i),
),
});
}
return {
status: "error",
errorKind: "diagnostics",
diagnostics,
};
} else {
return {
status: "error",
errorKind: "plain",
message: readCString(w.haku_status_string(statusCode)),
};
}
}
let pDefsString = dup1to2(
w.haku_defs(this.#pInstance),
w.haku_defs_len(this.#pInstance),
1,
);
let pTagsString = dup1to2(
w.haku_tags(this.#pInstance),
w.haku_tags_len(this.#pInstance),
1,
);
this.#pDefs2 = allocCheck(
w2.haku2_defs_parse(
pDefsString.ptr,
w.haku_defs_len(this.#pInstance),
pTagsString.ptr,
w.haku_tags_len(this.#pInstance),
),
);
w2.haku2_scratch_reset(this.#pScratch2);
this.#pVm2 = allocCheck(w2.haku2_vm_new());
this.#bytecode2 = dup1to2(
w.haku_bytecode(this.#pInstance),
w.haku_bytecode_len(this.#pInstance),
1,
);
this.#localCount = w.haku_local_count(this.#pInstance);
freeString2(pDefsString);
freeString2(pTagsString);
return { status: "ok" };
}
#exceptionMessage() {
let len = w2.haku2_vm_exception_len(this.#pVm2);
let pExn = allocCheck(w2.haku2_alloc(len, 1));
w2.haku2_vm_exception_render(this.#pVm2, pExn);
let exn = readString(memory2, len, pExn);
w2.haku2_free(pExn, len, 1);
return exn;
}
#stackTrace() {
let count = w2.haku2_vm_stackframe_count(this.#pVm2);
let trace = [];
for (let i = 0; i < count; ++i) {
// Don't collect information about return marker stack frames.
// They don't actually represent function calls, they're only used to signal the .ret
// instruction at which the VM ought to halt.
let isReturnMarker = w2.haku2_vm_stackframe_is_return_marker(this.#pVm2, i) != 0;
if (isReturnMarker) continue;
let isSystem = w2.haku2_vm_stackframe_is_system(this.#pVm2, i) != 0;
let pc = w2.haku2_vm_stackframe_pc(this.#pVm2, i);
let span = null;
if (!isSystem && pc > 0) {
// NOTE: We find the span at (pc - 1), because stack frames' program counters are
// situated at where control flow _ought_ to return, and not where the function call
// is located. Therefore, to pull the program counter back into the function call,
// we subtract 1.
let pSpan = w.haku_bytecode_find_span(this.#pInstance, Math.max(0, pc - 1));
if (pSpan != 0) {
span = {
start: w.haku_span_start(pSpan),
end: w.haku_span_end(pSpan),
};
}
}
trace.push({
isSystem,
pc,
span,
functionName: readString(
memory2,
w2.haku2_vm_stackframe_function_name_len(this.#pVm2, i),
w2.haku2_vm_stackframe_function_name(this.#pVm2, i),
),
});
}
return trace;
}
#exceptionResult() {
return {
status: "error",
errorKind: "exception",
description: "Runtime error",
message: this.#exceptionMessage(),
stackTrace: this.#stackTrace(),
};
}
get ok() {
return this.#pVm2 != 0;
}
beginBrush() {
if (!this.ok) {
console.debug("VM instance is not available for drawing");
return { status: "ok" };
}
w2.haku2_scratch_reset(this.#pScratch2);
w2.haku2_vm_reset(this.#pVm2, this.#pScratch2, this.#pDefs2, this.#pLimits2, this.#fuel);
let ok = w2.haku2_vm_run_main(
this.#pVm2,
this.#pScratch2,
this.#bytecode2.ptr,
this.#bytecode2.size,
this.#localCount,
);
if (!ok) {
return this.#exceptionResult();
}
return { status: "ok" };
}
expectedContKind() {
if (w2.haku2_vm_is_dotter(this.#pVm2)) return ContKind.Dotter;
else return ContKind.Scribble;
}
contScribble(pixmap, translationX, translationY) {
w.haku_pixmap_set_translation(pixmap.ptr, translationX, translationY);
let ok = w2.haku2_render(this.#pVm2, pixmap.ptr, this.#renderMaxDepth);
if (!ok) {
return this.#exceptionResult();
} else {
return { status: "ok" };
}
}
contDotter({ fromX, fromY, toX, toY, num }) {
let ok = w2.haku2_vm_run_dotter(this.#pVm2, this.#pScratch2, fromX, fromY, toX, toY, num);
if (!ok) {
return this.#exceptionResult();
} else {
return { status: "ok" };
}
}
async evalBrush(options) {
let { runDotter, runScribble } = options;
let result;
result = this.beginBrush();
if (result.status != "ok") return result;
while (true) {
switch (this.expectedContKind()) {
case ContKind.Scribble:
result = await runScribble((pixmap, translationX, translationY) => {
return this.contScribble(pixmap, translationX, translationY);
});
return result;
case ContKind.Dotter:
let dotter = await runDotter();
result = this.contDotter(dotter);
if (result.status != "ok") return result;
break;
}
}
}
get astSize() {
return w.haku_stat_ast_size(this.#pInstance);
}
get remainingFuel() {
return this.ok ? w2.haku2_vm_fuel(this.#pVm2) : this.#fuel;
}
get usedMemory() {
return this.ok ? w2.haku2_scratch_used(this.#pScratch2) : 0;
}
}