diff --git a/js-runtime/.gitignore b/js-runtime/.gitignore new file mode 100644 index 00000000..ee7098f1 --- /dev/null +++ b/js-runtime/.gitignore @@ -0,0 +1,2 @@ +zig-out/ +zig-cache/ diff --git a/js-runtime/README.md b/js-runtime/README.md new file mode 100644 index 00000000..3e9f6982 --- /dev/null +++ b/js-runtime/README.md @@ -0,0 +1 @@ +# mach-js-runtime diff --git a/js-runtime/build.zig b/js-runtime/build.zig new file mode 100644 index 00000000..5ec65241 --- /dev/null +++ b/js-runtime/build.zig @@ -0,0 +1,22 @@ +const std = @import("std"); + +pub fn build(b: *std.build.Builder) void { + const mode = b.standardReleaseOptions(); + + const main_tests = b.addTest("src/main.zig"); + main_tests.addPackage(pkg); + main_tests.setBuildMode(mode); + + const test_step = b.step("test", "Run library tests"); + test_step.dependOn(&main_tests.step); +} + +pub const pkg = std.build.Pkg{ + .name = "js-runtime", + .source = .{ .path = thisDir() ++ "/src/main.zig" }, + .dependencies = &[_]std.build.Pkg{}, +}; + +fn thisDir() []const u8 { + return std.fs.path.dirname(@src().file) orelse "."; +} diff --git a/js-runtime/src/js-runtime.js b/js-runtime/src/js-runtime.js new file mode 100644 index 00000000..9ea3c20b --- /dev/null +++ b/js-runtime/src/js-runtime.js @@ -0,0 +1,338 @@ +let text_decoder = new TextDecoder(); +let text_encoder = new TextEncoder(); +let log_buf = ""; + +let uindex = 0; +let indices = []; +let values = []; +let value_map = {}; + +class MemoryBlock { + constructor(mem, offset = 0) { + this.mem = mem; + this.offset = offset; + } + + slice(offset) { + return new MemoryBlock(this.mem, offset); + } + + getMemory() { + return new DataView(this.mem, this.offset); + } + + getU8(offset) { + return this.getMemory().getUint8(offset, true); + } + + getU32(offset) { + return this.getMemory().getUint32(offset, true); + } + + getU64(offset) { + const ls = this.getU32(offset); + const ms = this.getU32(offset + 4); + + return ls + ms * 4294967296; + } + + getF64(offset) { + return this.getMemory().getFloat64(offset, true); + } + + getString(offset, len) { + return text_decoder.decode(new Uint8Array(this.mem, offset, len)); + } + + setU8(offset, data) { + this.getMemory().setUint8(offset, data, true); + } + + setU32(offset, data) { + this.getMemory().setUint32(offset, data, true); + } + + setU64(offset, data) { + this.getMemory().setUint32(offset, data, true); + this.getMemory().setUint32(offset + 4, Math.floor(data / 4294967296), true); + } + + setF64(offset, data) { + this.getMemory().setFloat64(offset, data, true); + } + + setString(offset, str) { + const string = text_encoder.encode(str); + const buffer = new Uint8Array(this.mem, offset, string.length); + for (let i = 0; i < string.length; i += 1) { + buffer[i] = string[i]; + } + } +} + +const zig = { + wasm: undefined, + buffer: undefined, + + init(wasm) { + this.wasm = wasm; + + values = []; + value_map = []; + this.addValue(globalThis); + }, + + addValue(value) { + value.__proto__.__uindex = uindex; + let idx = indices.pop(); + if (idx !== undefined) { + values[idx] = value; + } else { + idx = values.push(value) - 1; + } + value_map[uindex] = idx; + uindex += 1; + return idx; + }, + + zigCreateMap() { + return zig.addValue(new Map()); + }, + + zigCreateArray() { + return zig.addValue(new Array()); + }, + + zigCreateString(str, len) { + let memory = new MemoryBlock(zig.wasm.exports.memory.buffer); + return zig.addValue(memory.getString(str, len)); + }, + + zigCreateFunction(id) { + return zig.addValue(function () { + const args = zig.addValue(arguments); + zig.wasm.exports.wasmCallFunction(id, args, arguments.length); + const return_value = values[args]["return_value"]; + zig.zigCleanupObject(args); + return return_value; + }); + }, + + getType(value) { + switch (typeof value) { + case "object": + switch (value) { + case null: + return 4; + default: + return 0; + } + break; + case "number": + return 1; + case "boolean": + return 2; + case "string": + return 3; + case "undefined": + return 5; + case "function": + return 6; + } + }, + + writeObject(block, data, type) { + switch (type) { + case 0: + case 6: + block.setU8(0, type); + block.setU64(8, data); + break; + case 1: + block.setU8(0, 1); + block.setF64(8, data); + break; + case 2: + block.setU8(0, 2); + block.setU8(8, data); + break; + case 3: + block.setU8(0, 3); + block.setU64(8, data); + break; + case 4: + block.setU8(0, 4); + break; + case 5: + block.setU8(0, 5); + break; + } + }, + + readObject(block, memory) { + switch (block.getU8(0)) { + case 0: + case 7: + return values[block.getU64(8)]; + break; + case 1: + return block.getF64(8); + break; + case 2: + return Boolean(block.getU8(8)); + break; + case 3: + return values[block.getU64(8)]; + break; + case 4: + return null; + break; + case 5: + return undefined; + break; + } + }, + + getPropertyEx(prop, ret_ptr, offset) { + let len = undefined; + const type = this.getType(prop); + switch (type) { + case 3: + len = prop.length; + case 0: + case 6: + if (prop in value_map) { + prop = value_map[prop.__uindex]; + } else { + prop = zig.addValue(prop); + } + break; + } + + if (len !== undefined) prop.__proto__.length = len; + + let memory = new MemoryBlock(ret_ptr, offset); + zig.writeObject(memory, prop, type); + }, + + getProperty(prop, ret_ptr) { + return zig.getPropertyEx(prop, zig.wasm.exports.memory.buffer, ret_ptr); + }, + + zigGetProperty(id, name, len, ret_ptr) { + let memory = new MemoryBlock(zig.wasm.exports.memory.buffer); + let prop = values[id][memory.getString(name, len)]; + zig.getProperty(prop, ret_ptr); + }, + + zigSetProperty(id, name, len, set_ptr) { + let memory = new MemoryBlock(zig.wasm.exports.memory.buffer); + values[id][memory.getString(name, len)] = zig.readObject( + memory.slice(set_ptr), + memory + ); + }, + + zigDeleteProperty(id, name, len) { + let memory = new MemoryBlock(zig.wasm.exports.memory.buffer); + delete values[id][memory.getString(name, len)]; + }, + + zigGetIndex(id, index, ret_ptr) { + let prop = values[id][index]; + zig.getProperty(prop, ret_ptr); + }, + + zigSetIndex(id, index, set_ptr) { + let memory = new MemoryBlock(zig.wasm.exports.memory.buffer); + values[id][index] = zig.readObject(memory.slice(set_ptr), memory); + }, + + zigDeleteIndex(id, index) { + delete values[id][index]; + }, + + zigCleanupObject(id) { + const idx = Number(id); + delete value_map[values[idx].__uindex]; + delete values[idx]; + indices.push(idx); + }, + + zigGetStringLength(val_id) { + return values[value_map[val_id]].length; + }, + + zigGetString(val_id, ptr) { + let memory = new MemoryBlock(zig.wasm.exports.memory.buffer); + memory.setString(ptr, values[value_map[val_id]]); + }, + + zigFunctionCall(id, name, len, args, args_len, ret_ptr) { + let memory = new MemoryBlock(zig.wasm.exports.memory.buffer); + let argv = []; + for (let i = 0; i < args_len; i += 1) { + argv.push(zig.readObject(memory.slice(args + i * 16), memory)); + } + let result = values[id][memory.getString(name, len)].apply( + values[id], + argv + ); + + let length = undefined; + const type = zig.getType(result); + switch (type) { + case 3: + length = result.length; + case 0: + case 6: + result = zig.addValue(result); + break; + } + + if (length !== undefined) result.__proto__.length = length; + + zig.writeObject(memory.slice(ret_ptr), result, type); + }, + + zigFunctionInvoke(id, args, args_len, ret_ptr) { + let memory = new MemoryBlock(zig.wasm.exports.memory.buffer); + let argv = []; + for (let i = 0; i < args_len; i += 1) { + argv.push(zig.readObject(memory.slice(args + i * 16), memory)); + } + let result = values[id].apply(undefined, argv); + + let length = undefined; + const type = zig.getType(result); + switch (type) { + case 3: + length = result.length; + case 0: + case 6: + result = zig.addValue(result); + break; + } + + if (length !== undefined) result.__proto__.length = length; + + zig.writeObject(memory.slice(ret_ptr), result, type); + }, + + wzLogWrite(str, len) { + let memory = new MemoryBlock(zig.wasm.exports.memory.buffer); + log_buf += memory.getString(str, len); + }, + + wzLogFlush() { + console.log(log_buf); + log_buf = ""; + }, + + wzPanic(str, len) { + let memory = new MemoryBlock(zig.wasm.exports.memory.buffer); + throw Error(memory.getString(str, len)); + }, +}; + +export { zig }; diff --git a/js-runtime/src/main.zig b/js-runtime/src/main.zig new file mode 100644 index 00000000..b84f911d --- /dev/null +++ b/js-runtime/src/main.zig @@ -0,0 +1,204 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const builtin = @import("builtin"); + +const js = struct { + extern fn zigCreateMap() u32; + extern fn zigCreateArray() u32; + extern fn zigCreateString(str: [*]const u8, len: u32) u32; + extern fn zigCreateFunction(id: *const anyopaque) u32; + extern fn zigGetProperty(id: u64, name: [*]const u8, len: u32, ret_ptr: *anyopaque) void; + extern fn zigSetProperty(id: u64, name: [*]const u8, len: u32, set_ptr: *const anyopaque) void; + extern fn zigDeleteProperty(id: u64, name: [*]const u8, len: u32) void; + extern fn zigGetIndex(id: u64, index: u32, ret_ptr: *anyopaque) void; + extern fn zigSetIndex(id: u64, index: u32, set_ptr: *const anyopaque) void; + extern fn zigGetString(val_id: u64, ptr: [*]const u8) void; + extern fn zigGetStringLength(val_id: u64) u32; + extern fn zigDeleteIndex(id: u64, index: u32) void; + extern fn zigFunctionCall(id: u64, name: [*]const u8, len: u32, args: ?*const anyopaque, args_len: u32, ret_ptr: *anyopaque) void; + extern fn zigFunctionInvoke(id: u64, args: ?*const anyopaque, args_len: u32, ret_ptr: *anyopaque) void; + extern fn zigCleanupObject(id: u64) void; +}; + +pub const Value = extern struct { + tag: ValueTag, + val: extern union { + ref: u64, + num: f64, + bool: bool, + }, + + const ValueTag = enum(u8) { + ref, + num, + bool, + str, + nulled, + undef, + func_js, + func_zig, + }; + + pub const Tag = enum { + object, + num, + bool, + str, + nulled, + undef, + func, + }; + + pub fn is(val: *const Value, comptime tag: Tag) bool { + return switch (tag) { + .object => val.tag == .object, + .num => val.tag == .num, + .bool => val.tag == .bool, + .str => val.tag == .str, + .nulled => val.tag == .nulled, + .undef => val.tag == .undef, + .func => val.tag == .func_js or val.tag == .func_zig, + }; + } + + pub fn value(val: *const Value, comptime tag: Tag, allocator: ?std.mem.Allocator) switch (tag) { + .object => Object, + .num => f64, + .bool => bool, + .str => std.mem.Allocator.Error![]const u8, + .func => Function, + .nulled, .undef => @compileError("Cannot get null or undefined as a value"), + } { + return switch (tag) { + .object => Object{ .ref = val.val.ref }, + .num => val.val.num, + .bool => val.val.bool, + .str => blk: { + const len = js.zigGetStringLength(val.val.ref); + var slice = try allocator.?.alloc(u8, len); + js.zigGetString(val.val.ref, slice.ptr); + break :blk slice; + }, + .func => Function{ .ref = val.val.ref }, + else => unreachable, + }; + } +}; + +pub const Object = struct { + ref: u64, + + pub fn deinit(obj: *const Object) void { + js.zigCleanupObject(obj.ref); + } + + pub fn toValue(obj: *const Object) Value { + return .{ .tag = .ref, .val = .{ .ref = obj.ref } }; + } + + pub fn get(obj: *const Object, prop: []const u8) Value { + var ret: Value = undefined; + js.zigGetProperty(obj.ref, prop.ptr, @intCast(u32, prop.len), &ret); + return ret; + } + + pub fn set(obj: *const Object, prop: []const u8, value: Value) void { + js.zigSetProperty(obj.ref, prop.ptr, @intCast(u32, prop.len), &value); + } + + pub fn delete(obj: *const Object, prop: []const u8) void { + js.zigDeleteProperty(obj.ref, prop.ptr, @intCast(u32, prop.len)); + } + + pub fn getIndex(obj: *const Object, index: u32) Value { + var ret: Value = undefined; + js.zigGetIndex(obj.ref, index, &ret); + return ret; + } + + pub fn setIndex(obj: *const Object, index: u32, value: Value) void { + js.zigSetIndex(obj.ref, index, &value); + } + + pub fn deleteIndex(obj: *const Object, index: u32) void { + js.zigDeleteIndex(obj.ref, index); + } + + pub fn call(obj: *const Object, fun: []const u8, args: []const Value) Value { + var ret: Value = undefined; + js.zigFunctionCall(obj.ref, fun.ptr, fun.len, args.ptr, args.len, &ret); + return ret; + } +}; + +pub const Function = struct { + ref: u64, + + pub fn deinit(func: *const Function) void { + js.zigCleanupObject(func.ref); + } + + pub fn toValue(func: *const Function) Value { + return .{ .tag = .func_zig, .val = .{ .ref = func.ref } }; + } + + pub fn invoke(func: *const Function, args: []const Value) Value { + var ret: Value = undefined; + js.zigFunctionInvoke(func.ref, args.ptr, args.len, &ret); + return ret; + } +}; + +export fn wasmCallFunction(id: *anyopaque, args: u32, len: u32) void { + const obj = Object{ .ref = args }; + if (builtin.zig_backend == .stage1) { + obj.set("return_value", functions.items[@ptrToInt(id) - 1](obj, len)); + } else { + var func = @ptrCast(*FunType, @alignCast(std.meta.alignment(*FunType), id)); + obj.set("return_value", func(obj, len)); + } +} + +pub fn global() Object { + return Object{ .ref = 0 }; +} + +pub fn createMap() Object { + return .{ .ref = js.zigCreateMap() }; +} + +pub fn createArray() Object { + return .{ .ref = js.zigCreateArray() }; +} + +pub fn createString(string: []const u8) Value { + return .{ .tag = .str, .val = .{ .ref = js.zigCreateString(string.ptr, string.len) } }; +} + +pub fn createNumber(num: f64) Value { + return .{ .tag = .num, .val = .{ .num = num } }; +} + +pub fn createBool(val: bool) Value { + return .{ .tag = .bool, .val = .{ .bool = val } }; +} + +pub fn createNull() Value { + return .{ .tag = .nulled, .val = undefined }; +} + +pub fn createUndefined() Value { + return .{ .tag = .undef, .val = undefined }; +} + +const FunType = fn (args: Object, args_len: u32) Value; + +var functions: std.ArrayListUnmanaged(FunType) = .{}; + +pub fn createFunction(fun: FunType) Function { + if (builtin.zig_backend == .stage1) { + functions.append(std.heap.page_allocator, fun) catch unreachable; + return .{ .ref = js.zigCreateFunction(@intToPtr(*anyopaque, functions.items.len)) }; + } + return .{ .ref = js.zigCreateFunction(&fun) }; +}