diff --git a/manifest.scm b/manifest.scm index 3733392..21af7ff 100644 --- a/manifest.scm +++ b/manifest.scm @@ -1,28 +1,8 @@ (use-modules (guix git-download) (guix packages) - (gnu packages autotools) (gnu packages base) (gnu packages compression) (gnu packages guile) - (gnu packages guile-xyz) - (gnu packages pkg-config) - (gnu packages texinfo)) + (gnu packages guile-xyz)) -(define guile-hoot-next - (let ((commit "66eca43e7fc0478b265c7826b7e52eec866e0b21") - (revision "1")) - (package - (inherit guile-hoot) - (version (git-version "0.4.1" revision commit)) - (source (origin - (method git-fetch) - (uri (git-reference - (url "https://gitlab.com/spritely/guile-hoot.git") - (commit commit))) - (file-name (git-file-name "guile-hoot" version)) - (sha256 - (base32 "0pd81wg6adzf1kvmgcxylailzwa0d5vdrhfpy4d54ddl31j6h824")))) - (native-inputs - (list autoconf automake pkg-config texinfo))))) - -(packages->manifest (list guile-next guile-hoot-next gnu-make zip)) +(packages->manifest (list guile-next guile-hoot gnu-make zip)) diff --git a/reflect.js b/reflect.js index b38867f..af6b0da 100644 --- a/reflect.js +++ b/reflect.js @@ -141,7 +141,7 @@ class Port extends HeapObject { toString() { return "#"; } } class Struct extends HeapObject { toString() { return "#"; } } function instantiate_streaming(path, imports) { - if (typeof fetch !== 'undefined') + if (typeof fetch !== 'undefined' && typeof window !== 'undefined') return WebAssembly.instantiateStreaming(fetch(path), imports); let bytes; if (typeof read !== 'undefined') { @@ -155,6 +155,168 @@ function instantiate_streaming(path, imports) { return WebAssembly.instantiate(bytes, imports); } +class IterableWeakSet { + #array; + #set; + constructor() { this.#array = []; this.#set = new WeakSet; } + #kill(i) { + let tail = this.#array.pop(); + if (i < this.#array.length) + this.#array[i] = tail; + } + #get(i) { + if (i >= this.#array.length) + return null; + let obj = this.#array[i].deref(); + if (obj) + return obj; + this.#kill(i); + return null; + } + #cleanup() { + let i = 0; + while (this.#get(i)) i++; + } + has(x) { return this.#set.has(x); } + add(x) { + if (this.has(x)) + return; + if (this.#array.length % 32 == 0) + this.#cleanup(); + this.#set.add(x); + this.#array.push(new WeakRef(x)); + } + delete(x) { + if (!this.has(x)) + return; + this.#set.delete(x); + let i = 0; + while (this.#get(i) != x) i++; + this.#kill(i); + } + *[Symbol.iterator]() { + for (let i = 0, x; x = this.#get(i); i++) + yield x; + } +} + +// See: +// https://github.com/tc39/proposal-weakrefs?tab=readme-ov-file#iterable-weakmaps +class IterableWeakMap { + #weakMap = new WeakMap(); + #refSet = new Set(); + #finalizationGroup = new FinalizationRegistry(IterableWeakMap.#cleanup); + + static #cleanup({ set, ref }) { + set.delete(ref); + } + + constructor(iterable) { + for (const [key, value] of iterable) { + this.set(key, value); + } + } + + set(key, value) { + const ref = new WeakRef(key); + + this.#weakMap.set(key, { value, ref }); + this.#refSet.add(ref); + this.#finalizationGroup.register(key, { + set: this.#refSet, + ref + }, ref); + } + + get(key) { + const entry = this.#weakMap.get(key); + return entry && entry.value; + } + + delete(key) { + const entry = this.#weakMap.get(key); + if (!entry) { + return false; + } + + this.#weakMap.delete(key); + this.#refSet.delete(entry.ref); + this.#finalizationGroup.unregister(entry.ref); + return true; + } + + *[Symbol.iterator]() { + for (const ref of this.#refSet) { + const key = ref.deref(); + if (!key) continue; + const { value } = this.#weakMap.get(key); + yield [key, value]; + } + } + + entries() { + return this[Symbol.iterator](); + } + + *keys() { + for (const [key, value] of this) { + yield key; + } + } + + *values() { + for (const [key, value] of this) { + yield value; + } + } +} +// class IterableWeakMap { +// #array; +// #map; +// constructor() { this.#array = []; this.#map = new WeakMap; } +// #kill(i) { +// let tail = this.#array.pop(); +// if (i < this.#array.length) +// this.#array[i] = tail; +// } +// #geti(i) { +// // If a dead weak ref is at i then kill it and try again with +// // the next weak ref. +// while (true) { +// if (i >= this.#array.length) +// return null; +// let obj = this.#array[i].deref(); +// if (obj) +// return obj; +// this.#kill(i); +// } +// } +// #cleanup() { +// let i = 0; +// while (this.#get(i)) i++; +// } +// has(k) { return this.#map.has(k); } +// get(k) { return this.#map.get(k); } +// set(k, v) { +// if (this.#array.length % 32 == 0) +// this.#cleanup(); +// this.#map.set(k, v); +// this.#array.push(new WeakRef(k)); +// } +// delete(k) { +// if (!this.has(k)) +// return; +// this.#map.delete(k); +// let i = 0; +// while (this.#geti(i) != k) i++; +// this.#kill(i); +// } +// *[Symbol.iterator]() { +// for (let i = 0, k; k = this.#geti(i); i++) +// yield [k, this.get(k)]; +// } +// } + class Scheme { #instance; #abi; @@ -170,9 +332,11 @@ class Scheme { debug_str_scm: (x, y) => { console.log(`reflect debug: ${x}: #`); }, + code_source(x) { return ['???', 0, 0]; } }; let reflect_wasm = reflect_wasm_dir + '/reflect.wasm'; let rt = { + quit(status) { throw new SchemeQuitError(status); }, die(tag, data) { throw new SchemeTrapError(tag, data); }, wtf8_to_string(wtf8) { return wtf8_to_string(wtf8); }, string_to_wtf8(str) { return string_to_wtf8(str); }, @@ -348,6 +512,11 @@ class SchemeTrapError extends Error { toString() { return `SchemeTrap(${this.tag}, )`; } } +class SchemeQuitError extends Error { + constructor(status) { super(); this.status = status; } + toString() { return `SchemeQuit(status=${this.status})`; } +} + function string_repr(str) { // FIXME: Improve to match Scheme. return '"' + str.replace(/(["\\])/g, '\\$1').replace(/\n/g, '\\n') + '"'; @@ -415,6 +584,18 @@ async function load_wtf8_helper_module(reflect_wasm_dir = '') { wtf8_helper = instance; } +function make_textual_writable_stream(write_chars) { + const decoder = new TextDecoder("utf-8"); + return new WritableStream({ + write(chunk) { + return new Promise((resolve, reject) => { + write_chars(decoder.decode(chunk, { stream: true })); + resolve(); + }); + } + }); +} + class SchemeModule { #instance; #io_handler; @@ -469,7 +650,6 @@ class SchemeModule { bignum_logand(a, b) { return BigInt(a) & BigInt(b); }, bignum_logior(a, b) { return BigInt(a) | BigInt(b); }, bignum_logxor(a, b) { return BigInt(a) ^ BigInt(b); }, - bignum_logsub(a, b) { return BigInt(a) & (~ BigInt(b)); }, bignum_lt(a, b) { return a < b; }, bignum_le(a, b) { return a <= b; }, @@ -477,14 +657,17 @@ class SchemeModule { bignum_to_f64(n) { return Number(n); }, - f64_is_nan(n) { return Number.isNaN(n); }, - f64_is_infinite(n) { return !Number.isFinite(n); }, - flonum_to_string, string_upcase: Function.call.bind(String.prototype.toUpperCase), string_downcase: Function.call.bind(String.prototype.toLowerCase), + make_weak_ref(x) { return new WeakRef(x); }, + weak_ref_deref(ref, fail) { + const val = ref.deref(); + return val === undefined ? fail: val; + }, + make_weak_map() { return new WeakMap; }, weak_map_get(map, k, fail) { const val = map.get(k); @@ -493,6 +676,14 @@ class SchemeModule { weak_map_set(map, k, v) { return map.set(k, v); }, weak_map_delete(map, k) { return map.delete(k); }, + make_finalization_registry(f) { return new FinalizationRegistry(f); }, + finalization_registry_register(registry, target, heldValue, unregisterToken) { + registry.register(target, heldValue, unregisterToken); + }, + finalization_registry_unregister(registry, unregisterToken) { + registry.unregister(unregisterToken); + }, + fsqrt: Math.sqrt, fsin: Math.sin, fcos: Math.cos, @@ -510,7 +701,15 @@ class SchemeModule { async_invoke, async_invoke_later, - promise_on_completed(p, kt, kf) { p.then(kt, kf); }, + promise_on_completed(p, kt, kf) { + p.then((val) => { + if (val === undefined) { + kt(false); + } else { + kt(val); + } + }, kf); + }, promise_complete(callback, val) { callback(val); }, // Wrap in functions to allow for lazy loading of the wtf8 @@ -518,11 +717,73 @@ class SchemeModule { wtf8_to_string(wtf8) { return wtf8_to_string(wtf8); }, string_to_wtf8(str) { return string_to_wtf8(str); }, - die(tag, data) { throw new SchemeTrapError(tag, data); } + make_regexp(pattern, flags) { return new RegExp(pattern, flags); }, + regexp_exec(re, str) { return re.exec(str); }, + regexp_match_string(m) { return m.input; }, + regexp_match_start(m) { return m.index; }, + regexp_match_end(m) { return m.index + m[0].length; }, + regexp_match_count(m) { return m.length; }, + regexp_match_substring(m, i) { + const str = m[i]; + if (str === undefined) { + return null; + } + return str; + }, + + die(tag, data) { throw new SchemeTrapError(tag, data); }, + quit(status) { throw new SchemeQuitError(status); }, + + stream_make_chunk(len) { return new Uint8Array(len); }, + stream_chunk_length(chunk) { return chunk.length; }, + stream_chunk_ref(chunk, idx) { return chunk[idx]; }, + stream_chunk_set(chunk, idx, val) { chunk[idx] = val; }, + stream_get_reader(stream) { return stream.getReader(); }, + stream_read(reader) { return reader.read(); }, + stream_result_chunk(result) { return result.value; }, + stream_result_done(result) { return result.done ? 1 : 0; }, + stream_get_writer(stream) { return stream.getWriter(); }, + stream_write(writer, chunk) { return writer.write(chunk); }, + stream_close_writer(writer) { return writer.close(); }, }; + static #code_origins = new WeakMap; + static #all_modules = new IterableWeakSet; + static #code_origin(code) { + if (SchemeModule.#code_origins.has(code)) + return SchemeModule.#code_origins.get(code); + for (let mod of SchemeModule.#all_modules) { + for (let i = 0, x = null; x = mod.instance_code(i); i++) { + let origin = [mod, i]; + if (!SchemeModule.#code_origins.has(x)) + SchemeModule.#code_origins.set(x, origin); + if (x === code) + return origin; + } + } + return [null, 0]; + } + + static #code_name(code) { + let [mod, idx] = SchemeModule.#code_origin(code); + if (mod) + return mod.instance_code_name(idx); + return null; + } + + static #code_source(code) { + let [mod, idx] = SchemeModule.#code_origin(code); + if (mod) + return mod.instance_code_source(idx); + return [null, 0, 0]; + } + constructor(instance) { + SchemeModule.#all_modules.add(this); this.#instance = instance; + let open_file_error = (filename) => { + throw new Error('No file system access'); + }; if (typeof printErr === 'function') { // v8/sm dev console // On the console, try to use 'write' (v8) or 'putstr' (sm), // as these don't add an extraneous newline. Unfortunately @@ -541,14 +802,13 @@ class SchemeModule { return '\n'; } }: () => ''; - let delete_file = (filename) => false; this.#io_handler = { write_stdout: write_no_newline, write_stderr: printErr, read_stdin, file_exists: (filename) => false, - open_input_file: (filename) => {}, - open_output_file: (filename) => {}, + open_input_file: open_file_error, + open_output_file: open_file_error, close_file: () => undefined, read_file: (handle, length) => 0, write_file: (handle, length) => 0, @@ -557,7 +817,11 @@ class SchemeModule { file_buffer_size: (handle) => 0, file_buffer_ref: (handle, i) => 0, file_buffer_set: (handle, i, x) => undefined, - delete_file: (filename) => undefined + delete_file: (filename) => undefined, + // FIXME: We should polyfill these out. + stream_stdin() { throw new Error('stream_stdin not implemented'); }, + stream_stdout() { throw new Error('stream_stderr not implemented'); }, + stream_stderr() { throw new Error('stream_stderr not implemented'); }, }; } else if (typeof window !== 'undefined') { // web browser this.#io_handler = { @@ -565,8 +829,8 @@ class SchemeModule { write_stderr: console.error, read_stdin: () => '', file_exists: (filename) => false, - open_input_file: (filename) => {}, - open_output_file: (filename) => {}, + open_input_file: open_file_error, + open_output_file: open_file_error, close_file: () => undefined, read_file: (handle, length) => 0, write_file: (handle, length) => 0, @@ -575,17 +839,26 @@ class SchemeModule { file_buffer_size: (handle) => 0, file_buffer_ref: (handle, i) => 0, file_buffer_set: (handle, i, x) => undefined, - delete_file: (filename) => undefined + delete_file: (filename) => undefined, + stream_stdin() { return new ReadableStream; }, + stream_stdout() { + return make_textual_writable_stream(s => console.log(s)); + }, + stream_stderr() { + return make_textual_writable_stream(s => console.error(s)); + }, }; } else { // nodejs const fs = require('fs'); const process = require('process'); + const { ReadableStream, WritableStream } = require('node:stream/web'); + const bufLength = 1024; const stdinBuf = Buffer.alloc(bufLength); const SEEK_SET = 0, SEEK_CUR = 1, SEEK_END = 2; this.#io_handler = { - write_stdout: console.log, - write_stderr: console.error, + write_stdout: process.stdout.write.bind(process.stdout), + write_stderr: process.stderr.write.bind(process.stderr), read_stdin: () => { let n = fs.readSync(process.stdin.fd, stdinBuf, 0, stdinBuf.length); return stdinBuf.toString('utf8', 0, n); @@ -646,7 +919,23 @@ class SchemeModule { file_buffer_set: (handle, i, x) => { handle.buf[i] = x; }, - delete_file: fs.rmSync.bind(fs) + delete_file: fs.rmSync.bind(fs), + stream_stdin() { + return new ReadableStream({ + async start(controller) { + for await (const chunk of process.stdin) { + controller.enqueue(chunk); + } + controller.close(); + } + }); + }, + stream_stdout() { + return make_textual_writable_stream(s => process.stdout.write(s)); + }, + stream_stderr() { + return make_textual_writable_stream(s => process.stderr.write(s)); + }, }; } this.#debug_handler = { @@ -673,12 +962,17 @@ class SchemeModule { file_buffer_size(handle) { return mod.#io_handler.file_buffer_size(handle); }, file_buffer_ref(handle, i) { return mod.#io_handler.file_buffer_ref(handle, i); }, file_buffer_set(handle, i, x) { return mod.#io_handler.file_buffer_set(handle, i, x); }, - delete_file(filename) { mod.#io_handler.delete_file(filename); } + delete_file(filename) { mod.#io_handler.delete_file(filename); }, + stream_stdin() { return mod.#io_handler.stream_stdin(); }, + stream_stdout() { return mod.#io_handler.stream_stdout(); }, + stream_stderr() { return mod.#io_handler.stream_stderr(); }, }; let debug = { debug_str(x) { mod.#debug_handler.debug_str(x); }, debug_str_i32(x, y) { mod.#debug_handler.debug_str_i32(x, y); }, debug_str_scm(x, y) { mod.#debug_handler.debug_str_scm(x, y); }, + code_name(code) { return SchemeModule.#code_name(code); }, + code_source(code) { return SchemeModule.#code_source(code); }, } let ffi = { procedure_to_extern(proc) { @@ -718,6 +1012,24 @@ class SchemeModule { return this.all_exports()[name]; throw new Error(`unknown export: ${name}`) } + instance_code(idx) { + if ('%instance-code' in this.all_exports()) { + return this.all_exports()['%instance-code'](idx); + } + return null; + } + instance_code_name(idx) { + if ('%instance-code-name' in this.all_exports()) { + return this.all_exports()['%instance-code-name'](idx); + } + return null; + } + instance_code_source(idx) { + if ('%instance-code-source' in this.all_exports()) { + return this.all_exports()['%instance-code-source'](idx); + } + return [null, 0, 0]; + } async reflect(opts = {}) { return await Scheme.reflect(this.exported_abi(), opts); } @@ -734,3 +1046,10 @@ function repr(obj) { return string_repr(obj); return obj + ''; } + +// Modulize when possible. +if (typeof exports !== 'undefined') { + exports.Scheme = Scheme; + exports.SchemeQuitError = SchemeQuitError; + exports.repr = repr; +} diff --git a/reflect.wasm b/reflect.wasm index 770c94c..2513e8c 100644 Binary files a/reflect.wasm and b/reflect.wasm differ