// -*- js2-basic-offset: 4 -*- class Char { constructor(codepoint) { this.codepoint = codepoint; } toString() { let ch = String.fromCodePoint(this.codepoint); if (ch.match(/[a-zA-Z0-9$[\]().]/)) return `#\\${ch}`; return `#\\x${this.codepoint.toString(16)}`; } } class Eof { toString() { return "#"; } } class Nil { toString() { return "#nil"; } } class Null { toString() { return "()"; } } class Unspecified { toString() { return "#"; } } class Complex { constructor(real, imag) { this.real = real; this.imag = imag; } toString() { const sign = this.imag >= 0 && Number.isFinite(this.imag) ? "+": ""; return `${flonum_to_string(this.real)}${sign}${flonum_to_string(this.imag)}i`; } } class Fraction { constructor(num, denom) { this.num = num; this.denom = denom; } toString() { return `${this.num}/${this.denom}`; } } class HeapObject { constructor(reflector, obj) { this.reflector = reflector; this.obj = obj; } repr() { return this.toString(); } // Default implementation. } class Pair extends HeapObject { toString() { return "#"; } repr() { let car_repr = repr(this.reflector.car(this)); let cdr_repr = repr(this.reflector.cdr(this)); if (cdr_repr == '()') return `(${car_repr})`; if (cdr_repr.charAt(0) == '(') return `(${car_repr} ${cdr_repr.substring(1)}`; return `(${car_repr} . ${cdr_repr})`; } } class MutablePair extends Pair { toString() { return "#"; } } class Vector extends HeapObject { toString() { return "#"; } repr() { let len = this.reflector.vector_length(this); let out = '#('; for (let i = 0; i < len; i++) { if (i) out += ' '; out += repr(this.reflector.vector_ref(this, i)); } out += ')'; return out; } } class MutableVector extends Vector { toString() { return "#"; } } class Bytevector extends HeapObject { toString() { return "#"; } repr() { let len = this.reflector.bytevector_length(this); let out = '#vu8('; for (let i = 0; i < len; i++) { if (i) out += ' '; out += this.reflector.bytevector_ref(this, i); } out += ')'; return out; } } class MutableBytevector extends Bytevector { toString() { return "#"; } } class Bitvector extends HeapObject { toString() { return "#"; } repr() { let len = this.reflector.bitvector_length(this); let out = '#*'; for (let i = 0; i < len; i++) { out += this.reflector.bitvector_ref(this, i) ? '1' : '0'; } return out; } } class MutableBitvector extends Bitvector { toString() { return "#"; } } class MutableString extends HeapObject { toString() { return "#"; } repr() { return string_repr(this.reflector.string_value(this)); } } class Procedure extends HeapObject { toString() { return "#"; } call(...arg) { return this.reflector.call(this, ...arg); } } class Sym extends HeapObject { toString() { return "#"; } repr() { return this.reflector.symbol_name(this); } } class Keyword extends HeapObject { toString() { return "#"; } repr() { return `#:${this.reflector.keyword_name(this)}`; } } class Variable extends HeapObject { toString() { return "#"; } } class AtomicBox extends HeapObject { toString() { return "#"; } } class HashTable extends HeapObject { toString() { return "#"; } } class WeakTable extends HeapObject { toString() { return "#"; } } class Fluid extends HeapObject { toString() { return "#"; } } class DynamicState extends HeapObject { toString() { return "#"; } } class Syntax extends HeapObject { toString() { return "#"; } } class Port extends HeapObject { toString() { return "#"; } } class Struct extends HeapObject { toString() { return "#"; } } function instantiate_streaming(path, imports) { if (typeof fetch !== 'undefined') return WebAssembly.instantiateStreaming(fetch(path), imports); let bytes; if (typeof read !== 'undefined') { bytes = read(path, 'binary'); } else if (typeof readFile !== 'undefined') { bytes = readFile(path); } else { let fs = require('fs'); bytes = fs.readFileSync(path); } return WebAssembly.instantiate(bytes, imports); } class Scheme { #instance; #abi; constructor(instance, abi) { this.#instance = instance; this.#abi = abi; } static async reflect(abi) { let debug = { debug_str(x) { console.log(`reflect debug: ${x}`); }, debug_str_i32(x, y) { console.log(`reflect debug: ${x}: ${y}`); }, debug_str_scm: (x, y) => { console.log(`reflect debug: ${x}: #`); }, }; let { module, instance } = await instantiate_streaming('js-runtime/reflect.wasm', { abi, debug, rt: { 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); }, } }); return new Scheme(instance, abi); } #init_module(mod) { mod.set_debug_handler({ debug_str(x) { console.log(`debug: ${x}`); }, debug_str_i32(x, y) { console.log(`debug: ${x}: ${y}`); }, debug_str_scm: (x, y) => { console.log(`debug: ${x}: ${repr(this.#to_js(y))}`); }, }); mod.set_ffi_handler({ procedure_to_extern: (obj) => { const proc = this.#to_js(obj); return (...args) => { return proc.call(...args); }; } }); let proc = new Procedure(this, mod.get_export('$load').value); return proc.call(); } static async load_main(path, abi, user_imports = {}) { let mod = await SchemeModule.fetch_and_instantiate(path, abi, user_imports); let reflect = await mod.reflect(); return reflect.#init_module(mod); } async load_extension(path, user_imports = {}) { let mod = await SchemeModule.fetch_and_instantiate(path, this.#abi, user_imports); return this.#init_module(mod); } #to_scm(js) { let api = this.#instance.exports; if (typeof(js) == 'number') { return api.scm_from_f64(js); } else if (typeof(js) == 'bigint') { if (BigInt(api.scm_most_negative_fixnum()) <= js && js <= BigInt(api.scm_most_positive_fixnum())) return api.scm_from_fixnum(Number(js)); return api.scm_from_bignum(js); } else if (typeof(js) == 'boolean') { return js ? api.scm_true() : api.scm_false(); } else if (typeof(js) == 'string') { return api.scm_from_string(js); } else if (typeof(js) == 'object') { if (js instanceof Eof) return api.scm_eof(); if (js instanceof Nil) return api.scm_nil(); if (js instanceof Null) return api.scm_null(); if (js instanceof Unspecified) return api.scm_unspecified(); if (js instanceof Char) return api.scm_from_char(js.codepoint); if (js instanceof HeapObject) return js.obj; if (js instanceof Fraction) return api.scm_from_fraction(this.#to_scm(js.num), this.#to_scm(js.denom)); if (js instanceof Complex) return api.scm_from_complex(js.real, js.imag); return api.scm_from_extern(js); } else { throw new Error(`unexpected; ${typeof(js)}`); } } #to_js(scm) { let api = this.#instance.exports; let descr = api.describe(scm); let handlers = { fixnum: () => BigInt(api.fixnum_value(scm)), char: () => new Char(api.char_value(scm)), true: () => true, false: () => false, eof: () => new Eof, nil: () => new Nil, null: () => new Null, unspecified: () => new Unspecified, flonum: () => api.flonum_value(scm), bignum: () => api.bignum_value(scm), complex: () => new Complex(api.complex_real(scm), api.complex_imag(scm)), fraction: () => new Fraction(this.#to_js(api.fraction_num(scm)), this.#to_js(api.fraction_denom(scm))), pair: () => new Pair(this, scm), 'mutable-pair': () => new MutablePair(this, scm), vector: () => new Vector(this, scm), 'mutable-vector': () => new MutableVector(this, scm), bytevector: () => new Bytevector(this, scm), 'mutable-bytevector': () => new MutableBytevector(this, scm), bitvector: () => new Bitvector(this, scm), 'mutable-bitvector': () => new MutableBitvector(this, scm), string: () => api.string_value(scm), 'mutable-string': () => new MutableString(this, scm), procedure: () => new Procedure(this, scm), symbol: () => new Sym(this, scm), keyword: () => new Keyword(this, scm), variable: () => new Variable(this, scm), 'atomic-box': () => new AtomicBox(this, scm), 'hash-table': () => new HashTable(this, scm), 'weak-table': () => new WeakTable(this, scm), fluid: () => new Fluid(this, scm), 'dynamic-state': () => new DynamicState(this, scm), syntax: () => new Syntax(this, scm), port: () => new Port(this, scm), struct: () => new Struct(this, scm), 'extern-ref': () => api.extern_value(scm) }; let handler = handlers[descr]; return handler ? handler() : scm; } call(func, ...args) { let api = this.#instance.exports; let argv = api.make_vector(args.length + 1, api.scm_false()); func = this.#to_scm(func); api.vector_set(argv, 0, func); for (let [idx, arg] of args.entries()) api.vector_set(argv, idx + 1, this.#to_scm(arg)); argv = api.call(func, argv); let results = []; for (let idx = 0; idx < api.vector_length(argv); idx++) results.push(this.#to_js(api.vector_ref(argv, idx))) return results; } car(x) { return this.#to_js(this.#instance.exports.car(x.obj)); } cdr(x) { return this.#to_js(this.#instance.exports.cdr(x.obj)); } vector_length(x) { return this.#instance.exports.vector_length(x.obj); } vector_ref(x, i) { return this.#to_js(this.#instance.exports.vector_ref(x.obj, i)); } bytevector_length(x) { return this.#instance.exports.bytevector_length(x.obj); } bytevector_ref(x, i) { return this.#instance.exports.bytevector_ref(x.obj, i); } bitvector_length(x) { return this.#instance.exports.bitvector_length(x.obj); } bitvector_ref(x, i) { return this.#instance.exports.bitvector_ref(x.obj, i) == 1; } string_value(x) { return this.#instance.exports.string_value(x.obj); } symbol_name(x) { return this.#instance.exports.symbol_name(x.obj); } keyword_name(x) { return this.#instance.exports.keyword_name(x.obj); } } class SchemeTrapError extends Error { constructor(tag, data) { super(); this.tag = tag; this.data = data; } // FIXME: data is raw Scheme object; would need to be reflected to // have a toString. toString() { return `SchemeTrap(${this.tag}, )`; } } function string_repr(str) { // FIXME: Improve to match Scheme. return '"' + str.replace(/(["\\])/g, '\\$1').replace(/\n/g, '\\n') + '"'; } function flonum_to_string(f64) { if (Object.is(f64, -0)) { return '-0.0'; } else if (Number.isFinite(f64)) { let repr = f64 + ''; return /^-?[0-9]+$/.test(repr) ? repr + '.0' : repr; } else if (Number.isNaN(f64)) { return '+nan.0'; } else { return f64 < 0 ? '-inf.0' : '+inf.0'; } } let wtf8_helper; function wtf8_to_string(wtf8) { let { as_iter, iter_next } = wtf8_helper.exports; let codepoints = []; let iter = as_iter(wtf8); for (let cp = iter_next(iter); cp != -1; cp = iter_next(iter)) codepoints.push(cp); // Passing too many codepoints can overflow the stack. let maxcp = 100000; if (codepoints.length <= maxcp) { return String.fromCodePoint(...codepoints); } // For converting large strings, concatenate several smaller // strings. let substrings = []; let end = 0; for (let start = 0; start != codepoints.length; start = end) { end = Math.min(start + maxcp, codepoints.length); substrings.push(String.fromCodePoint(...codepoints.slice(start, end))); } return substrings.join(''); } function string_to_wtf8(str) { let { string_builder, builder_push_codepoint, finish_builder } = wtf8_helper.exports; let builder = string_builder() for (let cp of str) builder_push_codepoint(builder, cp.codePointAt(0)); return finish_builder(builder); } async function load_wtf8_helper_module() { if (wtf8_helper) return; let { module, instance } = await instantiate_streaming("js-runtime/wtf8.wasm"); wtf8_helper = instance; } class SchemeModule { #instance; #io_handler; #debug_handler; #ffi_handler; static #rt = { bignum_from_string(str) { return BigInt(str); }, bignum_from_i32(n) { return BigInt(n); }, bignum_from_i64(n) { return n; }, bignum_from_u64(n) { return n < 0n ? 0xffff_ffff_ffff_ffffn + (n + 1n) : n; }, bignum_is_i64(n) { return -0x8000_0000_0000_0000n <= n && n <= 0x7FFF_FFFF_FFFF_FFFFn; }, bignum_is_u64(n) { return 0n <= n && n <= 0xFFFF_FFFF_FFFF_FFFFn; }, // This truncates; see https://tc39.es/ecma262/#sec-tobigint64. bignum_get_i64(n) { return n; }, bignum_add(a, b) { return BigInt(a) + BigInt(b) }, bignum_sub(a, b) { return BigInt(a) - BigInt(b) }, bignum_mul(a, b) { return BigInt(a) * BigInt(b) }, bignum_lsh(a, b) { return BigInt(a) << BigInt(b) }, bignum_rsh(a, b) { return BigInt(a) >> BigInt(b) }, bignum_quo(a, b) { return BigInt(a) / BigInt(b) }, bignum_rem(a, b) { return BigInt(a) % BigInt(b) }, bignum_mod(a, b) { let r = BigInt(a) % BigInt(b); if ((b > 0n && r < 0n) || (b < 0n && r > 0n)) { return b + r; } else { return r; } }, bignum_gcd(a, b) { a = BigInt(a); b = BigInt(b); if (a < 0n) { a = -a; } if (b < 0n) { b = -b; } if (a == 0n) { return b; } if (b == 0n) { return a; } let r; while (b != 0n) { r = a % b; a = b; b = r; } return a; }, 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; }, bignum_eq(a, b) { return a == b; }, 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_map() { return new WeakMap; }, weak_map_get(map, k, fail) { const val = map.get(k); return val === undefined ? fail: val; }, weak_map_set(map, k, v) { return map.set(k, v); }, weak_map_delete(map, k) { return map.delete(k); }, fsqrt: Math.sqrt, fsin: Math.sin, fcos: Math.cos, ftan: Math.tan, fasin: Math.asin, facos: Math.acos, fatan: Math.atan, fatan2: Math.atan2, flog: Math.log, fexp: Math.exp, jiffies_per_second() { return 1000000; }, current_jiffy() { return performance.now() * 1000; }, current_second() { return Date.now() / 1000; }, // Wrap in functions to allow for lazy loading of the wtf8 // module. 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); } }; constructor(instance) { this.#instance = instance; 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 // JSC doesn't have a printer that doesn't add a newline. let write_no_newline = typeof write === 'function' ? write : typeof putstr === 'function' ? putstr : print; // Use readline when available. v8 strips newlines so // we need to add them back. let read_stdin = typeof readline == 'function' ? () => { let line = readline(); if (line) { return `${line}\n`; } else { 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) => {}, close_file: () => undefined, read_file: (handle, length) => 0, write_file: (handle, length) => 0, seek_file: (handle, offset, whence) => -1, file_random_access: (handle) => false, file_buffer_size: (handle) => 0, file_buffer_ref: (handle, i) => 0, file_buffer_set: (handle, i, x) => undefined, delete_file: (filename) => undefined }; } else if (typeof window !== 'undefined') { // web browser this.#io_handler = { write_stdout: console.log, write_stderr: console.error, read_stdin: () => '', file_exists: (filename) => false, open_input_file: (filename) => {}, open_output_file: (filename) => {}, close_file: () => undefined, read_file: (handle, length) => 0, write_file: (handle, length) => 0, seek_file: (handle, offset, whence) => -1, file_random_access: (handle) => false, file_buffer_size: (handle) => 0, file_buffer_ref: (handle, i) => 0, file_buffer_set: (handle, i, x) => undefined, delete_file: (filename) => undefined }; } else { // nodejs const fs = require('fs'); const process = require('process'); 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, read_stdin: () => { let n = fs.readSync(process.stdin.fd, stdinBuf, 0, stdinBuf.length); return stdinBuf.toString('utf8', 0, n); }, file_exists: fs.existsSync.bind(fs), open_input_file: (filename) => { let fd = fs.openSync(filename, 'r'); return { fd, buf: Buffer.alloc(bufLength), pos: 0 }; }, open_output_file: (filename) => { let fd = fs.openSync(filename, 'w'); return { fd, buf: Buffer.alloc(bufLength), pos: 0 }; }, close_file: (handle) => { fs.closeSync(handle.fd); }, read_file: (handle, count) => { const n = fs.readSync(handle.fd, handle.buf, 0, count, handle.pos); handle.pos += n; return n; }, write_file: (handle, count) => { const n = fs.writeSync(handle.fd, handle.buf, 0, count, handle.pos); handle.pos += n; return n; }, seek_file: (handle, offset, whence) => { // There doesn't seem to be a way to ask NodeJS if // a position is valid or not. if (whence == SEEK_SET) { handle.pos = offset; return handle.pos; } else if (whence == SEEK_CUR) { handle.pos += offset; return handle.pos; } // SEEK_END not supported. return -1; }, file_random_access: (handle) => { return true; }, file_buffer_size: (handle) => { return handle.buf.length; }, file_buffer_ref: (handle, i) => { return handle.buf[i]; }, file_buffer_set: (handle, i, x) => { handle.buf[i] = x; }, delete_file: fs.rmSync.bind(fs) }; } this.#debug_handler = { debug_str(x) { console.log(`debug: ${x}`); }, debug_str_i32(x, y) { console.log(`debug: ${x}: ${y}`); }, debug_str_scm(x, y) { console.log(`debug: ${x}: #`); }, }; } static async fetch_and_instantiate(path, imported_abi, user_imports = {}) { await load_wtf8_helper_module(); let io = { write_stdout(str) { mod.#io_handler.write_stdout(str); }, write_stderr(str) { mod.#io_handler.write_stderr(str); }, read_stdin() { return mod.#io_handler.read_stdin(); }, file_exists(filename) { return mod.#io_handler.file_exists(filename); }, open_input_file(filename) { return mod.#io_handler.open_input_file(filename); }, open_output_file(filename) { return mod.#io_handler.open_output_file(filename); }, close_file(handle) { mod.#io_handler.close_file(handle); }, read_file(handle, length) { return mod.#io_handler.read_file(handle, length); }, write_file(handle, length) { return mod.#io_handler.write_file(handle, length); }, seek_file(handle, offset, whence) { return mod.#io_handler.seek_file(handle, offset, whence); }, file_random_access(handle) { return mod.#io_handler.file_random_access(handle); }, 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); } }; 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); }, } let ffi = { procedure_to_extern(proc) { return mod.#ffi_handler.procedure_to_extern(proc); } }; let imports = { rt: SchemeModule.#rt, abi: imported_abi, debug, io, ffi, ...user_imports }; let { module, instance } = await instantiate_streaming(path, imports); let mod = new SchemeModule(instance); return mod; } set_io_handler(h) { this.#io_handler = h; } set_debug_handler(h) { this.#debug_handler = h; } set_ffi_handler(h) { this.#ffi_handler = h; } all_exports() { return this.#instance.exports; } exported_abi() { let abi = {} for (let [k, v] of Object.entries(this.all_exports())) { if (k.startsWith("$")) abi[k] = v; } return abi; } exports() { let ret = {} for (let [k, v] of Object.entries(this.all_exports())) { if (!k.startsWith("$")) ret[k] = v; } return ret; } get_export(name) { if (name in this.all_exports()) return this.all_exports()[name]; throw new Error(`unknown export: ${name}`) } async reflect() { return await Scheme.reflect(this.exported_abi()); } } function repr(obj) { if (obj instanceof HeapObject) return obj.repr(); if (typeof obj === 'boolean') return obj ? '#t' : '#f'; if (typeof obj === 'number') return flonum_to_string(obj); if (typeof obj === 'string') return string_repr(obj); return obj + ''; }