diff --git a/Cargo.lock b/Cargo.lock index 7c282d8..e6a8ad8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -494,6 +494,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "combine" version = "4.6.7" @@ -2055,6 +2064,7 @@ dependencies = [ "bumpalo", "bzip2", "clap", + "colored", "criterion", "deno_core", "deno_error", diff --git a/nix-js/Cargo.toml b/nix-js/Cargo.toml index 1f08c84..c04788b 100644 --- a/nix-js/Cargo.toml +++ b/nix-js/Cargo.toml @@ -75,6 +75,7 @@ http-body-util = { version = "0.1", optional = true } http = { version = "1", optional = true } uuid = { version = "1", features = ["v4"], optional = true } ghost-cell = "0.2.6" +colored = "3.1.1" [features] inspector = ["dep:fastwebsockets", "dep:hyper", "dep:hyper-util", "dep:http-body-util", "dep:http", "dep:uuid"] diff --git a/nix-js/runtime-ts/src/builtins/io.ts b/nix-js/runtime-ts/src/builtins/io.ts index 895688a..510d207 100644 --- a/nix-js/runtime-ts/src/builtins/io.ts +++ b/nix-js/runtime-ts/src/builtins/io.ts @@ -16,6 +16,7 @@ import { CatchableError, isNixPath, NixPath } from "../types"; import { coerceToPath, coerceToString, StringCoercionMode } from "./conversion"; import { baseNameOf } from "./path"; import { isAttrs, isPath, isString } from "./type-check"; +import { execBytecode, execBytecodeScoped } from "../vm"; const importCache = new Map(); @@ -49,7 +50,8 @@ export const importFunc = (path: NixValue): NixValue => { return cached; } - const result = Deno.core.ops.op_import(pathStr); + const [code, currentDir] = Deno.core.ops.op_import(pathStr); + const result = execBytecode(code, currentDir); importCache.set(pathStr, result); return result; @@ -63,10 +65,8 @@ export const scopedImport = const pathStr = realisePath(path); - const code = Deno.core.ops.op_scoped_import(pathStr, scopeKeys); - - const scopedFunc = Function(`return (${code})`)(); - return scopedFunc(scopeAttrs); + const [code, currentDir] = Deno.core.ops.op_scoped_import(pathStr, scopeKeys); + return execBytecodeScoped(code, currentDir, scopeAttrs); }; export const storePath = (pathArg: NixValue): StringWithContext => { diff --git a/nix-js/runtime-ts/src/index.ts b/nix-js/runtime-ts/src/index.ts index c45fd6e..3e4cb5a 100644 --- a/nix-js/runtime-ts/src/index.ts +++ b/nix-js/runtime-ts/src/index.ts @@ -21,6 +21,7 @@ import { HAS_CONTEXT } from "./string-context"; import { createThunk, DEBUG_THUNKS, force, forceDeep, forceShallow, IS_CYCLE, IS_THUNK } from "./thunk"; import { forceBool } from "./type-assert"; import { IS_PATH, mkAttrs, mkFunction, type NixValue } from "./types"; +import { execBytecode, execBytecodeScoped, vmStrings, vmConstants } from "./vm"; export type NixRuntime = typeof Nix; @@ -55,6 +56,11 @@ export const Nix = { op, builtins, + strings: vmStrings, + constants: vmConstants, + execBytecode, + execBytecodeScoped, + replBindings, setReplBinding: (name: string, value: NixValue) => { replBindings.set(name, value); diff --git a/nix-js/runtime-ts/src/types/global.d.ts b/nix-js/runtime-ts/src/types/global.d.ts index 98ca49f..4988ebb 100644 --- a/nix-js/runtime-ts/src/types/global.d.ts +++ b/nix-js/runtime-ts/src/types/global.d.ts @@ -49,8 +49,8 @@ declare global { namespace Deno { namespace core { namespace ops { - function op_import(path: string): NixValue; - function op_scoped_import(path: string, scopeKeys: string[]): string; + function op_import(path: string): [Uint8Array, string]; + function op_scoped_import(path: string, scopeKeys: string[]): [Uint8Array, string]; function op_resolve_path(currentDir: string, path: string): string; diff --git a/nix-js/runtime-ts/src/vm.ts b/nix-js/runtime-ts/src/vm.ts new file mode 100644 index 0000000..9b63916 --- /dev/null +++ b/nix-js/runtime-ts/src/vm.ts @@ -0,0 +1,617 @@ +import { + assert, + call, + concatStringsWithContext, + hasAttr, + lookupWith, + mkPos, + resolvePath, + select, + selectWithDefault, +} from "./helpers"; +import { op } from "./operators"; +import { NixThunk } from "./thunk"; +import { forceBool } from "./type-assert"; +import { mkAttrs, NixArgs, type NixAttrs, type NixFunction, type NixValue } from "./types"; +import { builtins } from "./builtins"; + +enum Op { + PushConst = 0x01, + PushString = 0x02, + PushNull = 0x03, + PushTrue = 0x04, + PushFalse = 0x05, + + LoadLocal = 0x06, + LoadOuter = 0x07, + StoreLocal = 0x08, + AllocLocals = 0x09, + + MakeThunk = 0x0A, + MakeClosure = 0x0B, + MakePatternClosure = 0x0C, + + Call = 0x0D, + CallNoSpan = 0x0E, + + MakeAttrs = 0x0F, + MakeAttrsDyn = 0x10, + MakeEmptyAttrs = 0x11, + Select = 0x12, + SelectDefault = 0x13, + HasAttr = 0x14, + + MakeList = 0x15, + + OpAdd = 0x16, + OpSub = 0x17, + OpMul = 0x18, + OpDiv = 0x19, + OpEq = 0x20, + OpNeq = 0x21, + OpLt = 0x22, + OpGt = 0x23, + OpLeq = 0x24, + OpGeq = 0x25, + OpConcat = 0x26, + OpUpdate = 0x27, + + OpNeg = 0x28, + OpNot = 0x29, + + ForceBool = 0x30, + JumpIfFalse = 0x31, + JumpIfTrue = 0x32, + Jump = 0x33, + + ConcatStrings = 0x34, + ResolvePath = 0x35, + + Assert = 0x36, + + PushWith = 0x37, + PopWith = 0x38, + WithLookup = 0x39, + + LoadBuiltins = 0x40, + LoadBuiltin = 0x41, + + MkPos = 0x43, + + LoadReplBinding = 0x44, + LoadScopedBinding = 0x45, + + Return = 0x46, +} + +interface ScopeChain { + locals: NixValue[]; + parent: ScopeChain | null; +} + +interface WithScope { + env: NixValue; + last: WithScope | null; +} + +const strings: string[] = []; +const constants: NixValue[] = []; + +const $e: NixAttrs = new Map(); + +function readU16(code: Uint8Array, offset: number): number { + return code[offset] | (code[offset + 1] << 8); +} + +function readU32(code: Uint8Array, offset: number): number { + return ( + code[offset] | + (code[offset + 1] << 8) | + (code[offset + 2] << 16) | + (code[offset + 3] << 24) + ) >>> 0; +} + +function readI32(code: Uint8Array, offset: number): number { + return code[offset] | (code[offset + 1] << 8) | (code[offset + 2] << 16) | (code[offset + 3] << 24); +} + +export function execBytecode(code: Uint8Array, currentDir: string): NixValue { + const chain: ScopeChain = { locals: [], parent: null }; + return execFrame(code, 0, chain, currentDir, null, null); +} + +export function execBytecodeScoped( + code: Uint8Array, + currentDir: string, + scopeMap: NixAttrs, +): NixValue { + const chain: ScopeChain = { locals: [], parent: null }; + return execFrame(code, 0, chain, currentDir, null, scopeMap); +} + +function execFrame( + code: Uint8Array, + startPc: number, + chain: ScopeChain, + currentDir: string, + withScope: WithScope | null, + scopeMap: NixAttrs | null, +): NixValue { + const locals = chain.locals; + const stack: NixValue[] = []; + let pc = startPc; + + for (;;) { + const opcode = code[pc++]; + switch (opcode) { + case Op.PushConst: { + const idx = readU32(code, pc); + pc += 4; + stack.push(constants[idx]); + break; + } + case Op.PushString: { + const idx = readU32(code, pc); + pc += 4; + stack.push(strings[idx]); + break; + } + case Op.PushNull: + stack.push(null); + break; + case Op.PushTrue: + stack.push(true); + break; + case Op.PushFalse: + stack.push(false); + break; + + case Op.LoadLocal: { + const idx = readU32(code, pc); + pc += 4; + stack.push(locals[idx]); + break; + } + case Op.LoadOuter: { + const layer = code[pc++]; + const idx = readU32(code, pc); + pc += 4; + let c: ScopeChain = chain; + for (let i = 0; i < layer; i++) c = c.parent!; + stack.push(c.locals[idx]); + break; + } + case Op.StoreLocal: { + const idx = readU32(code, pc); + pc += 4; + locals[idx] = stack.pop()!; + break; + } + case Op.AllocLocals: { + const n = readU32(code, pc); + pc += 4; + for (let i = 0; i < n; i++) locals.push(null); + break; + } + + case Op.MakeThunk: { + const bodyPc = readU32(code, pc); + pc += 4; + const labelIdx = readU32(code, pc); + pc += 4; + const label = strings[labelIdx]; + const scopeChain = chain; + const scopeCode = code; + const scopeDir = currentDir; + const scopeWith = withScope; + stack.push( + new NixThunk( + () => execFrame(scopeCode, bodyPc, scopeChain, scopeDir, scopeWith, null), + label, + ), + ); + break; + } + case Op.MakeClosure: { + const bodyPc = readU32(code, pc); + pc += 4; + const nSlots = readU32(code, pc); + pc += 4; + const closureChain = chain; + const closureCode = code; + const closureDir = currentDir; + const closureWith = withScope; + const func: NixFunction = (arg: NixValue) => { + const innerLocals = new Array(1 + nSlots).fill(null); + innerLocals[0] = arg; + const innerChain: ScopeChain = { locals: innerLocals, parent: closureChain }; + return execFrame(closureCode, bodyPc, innerChain, closureDir, closureWith, null); + }; + stack.push(func); + break; + } + case Op.MakePatternClosure: { + const bodyPc = readU32(code, pc); + pc += 4; + const nSlots = readU32(code, pc); + pc += 4; + const nRequired = readU16(code, pc); + pc += 2; + const nOptional = readU16(code, pc); + pc += 2; + const hasEllipsis = code[pc++] !== 0; + + const required: string[] = []; + for (let i = 0; i < nRequired; i++) { + required.push(strings[readU32(code, pc)]); + pc += 4; + } + const optional: string[] = []; + for (let i = 0; i < nOptional; i++) { + optional.push(strings[readU32(code, pc)]); + pc += 4; + } + const positions = new Map(); + const nTotal = nRequired + nOptional; + for (let i = 0; i < nTotal; i++) { + const nameIdx = readU32(code, pc); + pc += 4; + const spanId = readU32(code, pc); + pc += 4; + positions.set(strings[nameIdx], spanId); + } + + const closureChain = chain; + const closureCode = code; + const closureDir = currentDir; + const closureWith = withScope; + const func: NixFunction = (arg: NixValue) => { + const innerLocals = new Array(1 + nSlots).fill(null); + innerLocals[0] = arg; + const innerChain: ScopeChain = { locals: innerLocals, parent: closureChain }; + return execFrame(closureCode, bodyPc, innerChain, closureDir, closureWith, null); + }; + func.args = new NixArgs(required, optional, positions, hasEllipsis); + stack.push(func); + break; + } + + case Op.Call: { + const spanId = readU32(code, pc); + pc += 4; + const arg = stack.pop()!; + const func = stack.pop()!; + stack.push(call(func, arg, spanId)); + break; + } + case Op.CallNoSpan: { + const arg = stack.pop()!; + const func = stack.pop()!; + stack.push(call(func, arg)); + break; + } + + case Op.MakeAttrs: { + const n = readU32(code, pc); + pc += 4; + const spanValues: number[] = []; + for (let i = 0; i < n; i++) { + spanValues.push(stack.pop() as number); + } + spanValues.reverse(); + const map: NixAttrs = new Map(); + const posMap = new Map(); + const pairs: [string, NixValue][] = []; + for (let i = 0; i < n; i++) { + const val = stack.pop()!; + const key = stack.pop() as string; + pairs.push([key, val]); + } + pairs.reverse(); + for (let i = 0; i < n; i++) { + map.set(pairs[i][0], pairs[i][1]); + posMap.set(pairs[i][0], spanValues[i]); + } + stack.push(mkAttrs(map, posMap)); + break; + } + case Op.MakeAttrsDyn: { + const nStatic = readU32(code, pc); + pc += 4; + const nDyn = readU32(code, pc); + pc += 4; + + const dynTriples: [NixValue, NixValue, number][] = []; + for (let i = 0; i < nDyn; i++) { + const dynSpan = stack.pop() as number; + const dynVal = stack.pop()!; + const dynKey = stack.pop()!; + dynTriples.push([dynKey, dynVal, dynSpan]); + } + dynTriples.reverse(); + + const spanValues: number[] = []; + for (let i = 0; i < nStatic; i++) { + spanValues.push(stack.pop() as number); + } + spanValues.reverse(); + + const map: NixAttrs = new Map(); + const posMap = new Map(); + const pairs: [string, NixValue][] = []; + for (let i = 0; i < nStatic; i++) { + const val = stack.pop()!; + const key = stack.pop() as string; + pairs.push([key, val]); + } + pairs.reverse(); + for (let i = 0; i < nStatic; i++) { + map.set(pairs[i][0], pairs[i][1]); + posMap.set(pairs[i][0], spanValues[i]); + } + + const dynKeys: NixValue[] = []; + const dynVals: NixValue[] = []; + const dynSpans: number[] = []; + for (const [k, v, s] of dynTriples) { + dynKeys.push(k); + dynVals.push(v); + dynSpans.push(s); + } + + stack.push(mkAttrs(map, posMap, { dynKeys, dynVals, dynSpans })); + break; + } + case Op.MakeEmptyAttrs: + stack.push($e); + break; + + case Op.Select: { + const nKeys = readU16(code, pc); + pc += 2; + const spanId = readU32(code, pc); + pc += 4; + const keys: NixValue[] = []; + for (let i = 0; i < nKeys; i++) keys.push(stack.pop()!); + keys.reverse(); + const obj = stack.pop()!; + stack.push(select(obj, keys, spanId)); + break; + } + case Op.SelectDefault: { + const nKeys = readU16(code, pc); + pc += 2; + const spanId = readU32(code, pc); + pc += 4; + const defaultVal = stack.pop()!; + const keys: NixValue[] = []; + for (let i = 0; i < nKeys; i++) keys.push(stack.pop()!); + keys.reverse(); + const obj = stack.pop()!; + stack.push(selectWithDefault(obj, keys, defaultVal, spanId)); + break; + } + case Op.HasAttr: { + const nKeys = readU16(code, pc); + pc += 2; + const keys: NixValue[] = []; + for (let i = 0; i < nKeys; i++) keys.push(stack.pop()!); + keys.reverse(); + const obj = stack.pop()!; + stack.push(hasAttr(obj, keys)); + break; + } + + case Op.MakeList: { + const count = readU32(code, pc); + pc += 4; + const items: NixValue[] = new Array(count); + for (let i = count - 1; i >= 0; i--) { + items[i] = stack.pop()!; + } + stack.push(items); + break; + } + + case Op.OpAdd: { + const b = stack.pop()!; + const a = stack.pop()!; + stack.push(op.add(a, b)); + break; + } + case Op.OpSub: { + const b = stack.pop()!; + const a = stack.pop()!; + stack.push(op.sub(a, b)); + break; + } + case Op.OpMul: { + const b = stack.pop()!; + const a = stack.pop()!; + stack.push(op.mul(a, b)); + break; + } + case Op.OpDiv: { + const b = stack.pop()!; + const a = stack.pop()!; + stack.push(op.div(a, b)); + break; + } + case Op.OpEq: { + const b = stack.pop()!; + const a = stack.pop()!; + stack.push(op.eq(a, b)); + break; + } + case Op.OpNeq: { + const b = stack.pop()!; + const a = stack.pop()!; + stack.push(!op.eq(a, b)); + break; + } + case Op.OpLt: { + const b = stack.pop()!; + const a = stack.pop()!; + stack.push(op.lt(a, b)); + break; + } + case Op.OpGt: { + const b = stack.pop()!; + const a = stack.pop()!; + stack.push(op.gt(a, b)); + break; + } + case Op.OpLeq: { + const b = stack.pop()!; + const a = stack.pop()!; + stack.push(!op.gt(a, b)); + break; + } + case Op.OpGeq: { + const b = stack.pop()!; + const a = stack.pop()!; + stack.push(!op.lt(a, b)); + break; + } + case Op.OpConcat: { + const b = stack.pop()!; + const a = stack.pop()!; + stack.push(op.concat(a, b)); + break; + } + case Op.OpUpdate: { + const b = stack.pop()!; + const a = stack.pop()!; + stack.push(op.update(a, b)); + break; + } + + case Op.OpNeg: { + const a = stack.pop()!; + stack.push(op.sub(0n, a)); + break; + } + case Op.OpNot: { + const a = stack.pop()!; + stack.push(!forceBool(a)); + break; + } + + case Op.ForceBool: { + const val = stack.pop()!; + stack.push(forceBool(val)); + break; + } + case Op.JumpIfFalse: { + const offset = readI32(code, pc); + pc += 4; + const val = stack.pop()!; + if (val === false) { + pc += offset; + } + break; + } + case Op.JumpIfTrue: { + const offset = readI32(code, pc); + pc += 4; + const val = stack.pop()!; + if (val === true) { + pc += offset; + } + break; + } + case Op.Jump: { + const offset = readI32(code, pc); + pc += 4; + pc += offset; + break; + } + + case Op.ConcatStrings: { + const nParts = readU16(code, pc); + pc += 2; + const forceString = code[pc++] !== 0; + const parts: NixValue[] = new Array(nParts); + for (let i = nParts - 1; i >= 0; i--) { + parts[i] = stack.pop()!; + } + stack.push(concatStringsWithContext(parts, forceString)); + break; + } + case Op.ResolvePath: { + const pathExpr = stack.pop()!; + stack.push(resolvePath(currentDir, pathExpr)); + break; + } + + case Op.Assert: { + const rawIdx = readU32(code, pc); + pc += 4; + const spanId = readU32(code, pc); + pc += 4; + const expr = stack.pop()!; + const assertion = stack.pop()!; + stack.push(assert(assertion, expr, strings[rawIdx], spanId)); + break; + } + + case Op.PushWith: { + const namespace = stack.pop()!; + withScope = { env: namespace, last: withScope }; + break; + } + case Op.PopWith: + withScope = withScope!.last; + break; + case Op.WithLookup: { + const nameIdx = readU32(code, pc); + pc += 4; + stack.push(lookupWith(strings[nameIdx], withScope!)); + break; + } + + case Op.LoadBuiltins: + stack.push(builtins); + break; + case Op.LoadBuiltin: { + const idx = readU32(code, pc); + pc += 4; + stack.push(builtins.get(strings[idx])!); + break; + } + + case Op.MkPos: { + const spanId = readU32(code, pc); + pc += 4; + stack.push(mkPos(spanId)); + break; + } + + case Op.LoadReplBinding: { + const idx = readU32(code, pc); + pc += 4; + stack.push(Nix.getReplBinding(strings[idx])); + break; + } + case Op.LoadScopedBinding: { + const idx = readU32(code, pc); + pc += 4; + stack.push(scopeMap!.get(strings[idx])!); + break; + } + + case Op.Return: + return stack.pop()!; + + default: + throw new Error(`Unknown bytecode opcode: ${opcode ? `0x${opcode.toString(16)}` : "undefined"} at pc=${pc - 1}`); + } + } +} + +declare const Nix: { + getReplBinding: (name: string) => NixValue; +}; + +export { strings as vmStrings, constants as vmConstants }; diff --git a/nix-js/src/bytecode.rs b/nix-js/src/bytecode.rs new file mode 100644 index 0000000..fc5aaea --- /dev/null +++ b/nix-js/src/bytecode.rs @@ -0,0 +1,906 @@ +use std::ops::Deref; +use std::path::Path; + +use hashbrown::HashMap; +use num_enum::TryFromPrimitive; +use rnix::TextRange; + +use crate::ir::{ArgId, Attr, BinOpKind, Ir, Param, RawIrRef, SymId, ThunkId, UnOpKind}; + +#[derive(Clone, Hash, Eq, PartialEq)] +pub(crate) enum Constant { + Int(i64), + Float(u64), +} + +pub struct Bytecode { + pub code: Box<[u8]>, + pub current_dir: String, +} + +pub(crate) trait BytecodeContext { + fn intern_string(&mut self, s: &str) -> u32; + fn intern_constant(&mut self, c: Constant) -> u32; + fn register_span(&self, range: TextRange) -> u32; + fn get_sym(&self, id: SymId) -> &str; + fn get_current_dir(&self) -> &Path; +} + +#[repr(u8)] +#[derive(Clone, Copy, TryFromPrimitive)] +#[allow(clippy::enum_variant_names)] +pub enum Op { + PushConst = 0x01, + PushString = 0x02, + PushNull = 0x03, + PushTrue = 0x04, + PushFalse = 0x05, + + LoadLocal = 0x06, + LoadOuter = 0x07, + StoreLocal = 0x08, + AllocLocals = 0x09, + + MakeThunk = 0x0A, + MakeClosure = 0x0B, + MakePatternClosure = 0x0C, + + Call = 0x0D, + CallNoSpan = 0x0E, + + MakeAttrs = 0x0F, + MakeAttrsDyn = 0x10, + MakeEmptyAttrs = 0x11, + Select = 0x12, + SelectDefault = 0x13, + HasAttr = 0x14, + + MakeList = 0x15, + + OpAdd = 0x16, + OpSub = 0x17, + OpMul = 0x18, + OpDiv = 0x19, + OpEq = 0x20, + OpNeq = 0x21, + OpLt = 0x22, + OpGt = 0x23, + OpLeq = 0x24, + OpGeq = 0x25, + OpConcat = 0x26, + OpUpdate = 0x27, + + OpNeg = 0x28, + OpNot = 0x29, + + ForceBool = 0x30, + JumpIfFalse = 0x31, + JumpIfTrue = 0x32, + Jump = 0x33, + + ConcatStrings = 0x34, + ResolvePath = 0x35, + + Assert = 0x36, + + PushWith = 0x37, + PopWith = 0x38, + WithLookup = 0x39, + + LoadBuiltins = 0x40, + LoadBuiltin = 0x41, + + MkPos = 0x43, + + LoadReplBinding = 0x44, + LoadScopedBinding = 0x45, + + Return = 0x46, +} + +struct ScopeInfo { + depth: u16, + arg_id: Option, + thunk_map: HashMap, +} + +struct BytecodeEmitter<'a, Ctx: BytecodeContext> { + ctx: &'a mut Ctx, + code: Vec, + scope_stack: Vec, +} + +pub(crate) fn compile_bytecode(ir: RawIrRef<'_>, ctx: &mut impl BytecodeContext) -> Bytecode { + let current_dir = ctx.get_current_dir().to_string_lossy().to_string(); + let mut emitter = BytecodeEmitter::new(ctx); + emitter.emit_toplevel(ir); + Bytecode { + code: emitter.code.into_boxed_slice(), + current_dir, + } +} + +pub(crate) fn compile_bytecode_scoped( + ir: RawIrRef<'_>, + ctx: &mut impl BytecodeContext, +) -> Bytecode { + let current_dir = ctx.get_current_dir().to_string_lossy().to_string(); + let mut emitter = BytecodeEmitter::new(ctx); + emitter.emit_toplevel_scoped(ir); + Bytecode { + code: emitter.code.into_boxed_slice(), + current_dir, + } +} + +impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { + fn new(ctx: &'a mut Ctx) -> Self { + Self { + ctx, + code: Vec::with_capacity(4096), + scope_stack: Vec::with_capacity(32), + } + } + + #[inline] + fn emit_op(&mut self, op: Op) { + self.code.push(op as u8); + } + + #[inline] + fn emit_u8(&mut self, val: u8) { + self.code.push(val); + } + + #[inline] + fn emit_u16(&mut self, val: u16) { + self.code.extend_from_slice(&val.to_le_bytes()); + } + + #[inline] + fn emit_u32(&mut self, val: u32) { + self.code.extend_from_slice(&val.to_le_bytes()); + } + + #[inline] + fn emit_i32_placeholder(&mut self) -> usize { + let offset = self.code.len(); + self.code.extend_from_slice(&[0u8; 4]); + offset + } + #[inline] + fn patch_i32(&mut self, offset: usize, val: i32) { + self.code[offset..offset + 4].copy_from_slice(&val.to_le_bytes()); + } + + #[inline] + fn emit_jump_placeholder(&mut self) -> usize { + self.emit_op(Op::Jump); + self.emit_i32_placeholder() + } + + #[inline] + fn patch_jump_target(&mut self, placeholder_offset: usize) { + let current_pos = self.code.len(); + let relative_offset = (current_pos as i32) - (placeholder_offset as i32) - 4; + self.patch_i32(placeholder_offset, relative_offset); + } + + fn current_depth(&self) -> u16 { + self.scope_stack.last().map_or(0, |s| s.depth) + } + + fn resolve_thunk(&self, id: ThunkId) -> (u16, u32) { + for scope in self.scope_stack.iter().rev() { + if let Some(&local_idx) = scope.thunk_map.get(&id) { + let layer = self.current_depth() - scope.depth; + return (layer, local_idx); + } + } + panic!("ThunkId {:?} not found in any scope", id); + } + + fn resolve_arg(&self, id: ArgId) -> (u16, u32) { + for scope in self.scope_stack.iter().rev() { + if scope.arg_id == Some(id) { + let layer = self.current_depth() - scope.depth; + return (layer, 0); + } + } + panic!("ArgId {:?} not found in any scope", id); + } + + fn emit_load(&mut self, layer: u16, local: u32) { + if layer == 0 { + self.emit_op(Op::LoadLocal); + self.emit_u32(local); + } else { + self.emit_op(Op::LoadOuter); + self.emit_u8(layer as u8); + self.emit_u32(local); + } + } + + fn count_with_thunks(&self, ir: RawIrRef<'_>) -> usize { + match ir.deref() { + Ir::With { thunks, body, .. } => thunks.len() + self.count_with_thunks(*body), + Ir::TopLevel { thunks, body } => thunks.len() + self.count_with_thunks(*body), + Ir::If { cond, consq, alter } => { + self.count_with_thunks(*cond) + + self.count_with_thunks(*consq) + + self.count_with_thunks(*alter) + } + Ir::BinOp { lhs, rhs, .. } => { + self.count_with_thunks(*lhs) + self.count_with_thunks(*rhs) + } + Ir::UnOp { rhs, .. } => self.count_with_thunks(*rhs), + Ir::Call { func, arg, .. } => { + self.count_with_thunks(*func) + self.count_with_thunks(*arg) + } + Ir::Assert { + assertion, expr, .. + } => self.count_with_thunks(*assertion) + self.count_with_thunks(*expr), + Ir::Select { expr, default, .. } => { + self.count_with_thunks(*expr) + default.map_or(0, |d| self.count_with_thunks(d)) + } + Ir::HasAttr { lhs, .. } => self.count_with_thunks(*lhs), + Ir::ConcatStrings { parts, .. } => { + parts.iter().map(|p| self.count_with_thunks(*p)).sum() + } + Ir::Path(p) => self.count_with_thunks(*p), + Ir::List { items } => items.iter().map(|item| self.count_with_thunks(*item)).sum(), + Ir::AttrSet { stcs, dyns } => { + stcs.iter() + .map(|(_, &(val, _))| self.count_with_thunks(val)) + .sum::() + + dyns + .iter() + .map(|&(k, v, _)| self.count_with_thunks(k) + self.count_with_thunks(v)) + .sum::() + } + _ => 0, + } + } + + fn collect_all_thunks<'ir>( + &self, + own_thunks: &[(ThunkId, RawIrRef<'ir>)], + body: RawIrRef<'ir>, + ) -> Vec<(ThunkId, RawIrRef<'ir>)> { + let mut all = Vec::from(own_thunks); + self.collect_with_thunks_recursive(body, &mut all); + let mut i = 0; + while i < all.len() { + let thunk_body = all[i].1; + self.collect_with_thunks_recursive(thunk_body, &mut all); + i += 1; + } + all + } + + fn collect_with_thunks_recursive<'ir>( + &self, + ir: RawIrRef<'ir>, + out: &mut Vec<(ThunkId, RawIrRef<'ir>)>, + ) { + match ir.deref() { + Ir::With { thunks, body, .. } => { + for &(id, inner) in thunks.iter() { + out.push((id, inner)); + } + self.collect_with_thunks_recursive(*body, out); + } + Ir::TopLevel { thunks, body } => { + for &(id, inner) in thunks.iter() { + out.push((id, inner)); + } + self.collect_with_thunks_recursive(*body, out); + } + Ir::If { cond, consq, alter } => { + self.collect_with_thunks_recursive(*cond, out); + self.collect_with_thunks_recursive(*consq, out); + self.collect_with_thunks_recursive(*alter, out); + } + Ir::BinOp { lhs, rhs, .. } => { + self.collect_with_thunks_recursive(*lhs, out); + self.collect_with_thunks_recursive(*rhs, out); + } + Ir::UnOp { rhs, .. } => self.collect_with_thunks_recursive(*rhs, out), + Ir::Call { func, arg, .. } => { + self.collect_with_thunks_recursive(*func, out); + self.collect_with_thunks_recursive(*arg, out); + } + Ir::Assert { + assertion, expr, .. + } => { + self.collect_with_thunks_recursive(*assertion, out); + self.collect_with_thunks_recursive(*expr, out); + } + Ir::Select { expr, default, .. } => { + self.collect_with_thunks_recursive(*expr, out); + if let Some(d) = default { + self.collect_with_thunks_recursive(*d, out); + } + } + Ir::HasAttr { lhs, .. } => self.collect_with_thunks_recursive(*lhs, out), + Ir::ConcatStrings { parts, .. } => { + for p in parts.iter() { + self.collect_with_thunks_recursive(*p, out); + } + } + Ir::Path(p) => self.collect_with_thunks_recursive(*p, out), + Ir::List { items } => { + for item in items.iter() { + self.collect_with_thunks_recursive(*item, out); + } + } + Ir::AttrSet { stcs, dyns } => { + for (_, &(val, _)) in stcs.iter() { + self.collect_with_thunks_recursive(val, out); + } + for &(key, val, _) in dyns.iter() { + self.collect_with_thunks_recursive(key, out); + self.collect_with_thunks_recursive(val, out); + } + } + _ => {} + } + } + + fn push_scope(&mut self, has_arg: bool, arg_id: Option, thunk_ids: &[ThunkId]) { + let depth = self.scope_stack.len() as u16; + let thunk_base = if has_arg { 1u32 } else { 0u32 }; + let thunk_map = thunk_ids + .iter() + .enumerate() + .map(|(i, &id)| (id, thunk_base + i as u32)) + .collect(); + self.scope_stack.push(ScopeInfo { + depth, + arg_id, + thunk_map, + }); + } + + fn pop_scope(&mut self) { + self.scope_stack.pop(); + } + + fn emit_toplevel(&mut self, ir: RawIrRef<'_>) { + match ir.deref() { + Ir::TopLevel { body, thunks } => { + let with_thunk_count = self.count_with_thunks(*body); + let total_slots = thunks.len() + with_thunk_count; + + let all_thunks = self.collect_all_thunks(thunks, *body); + let thunk_ids: Vec = all_thunks.iter().map(|&(id, _)| id).collect(); + + self.push_scope(false, None, &thunk_ids); + + if total_slots > 0 { + self.emit_op(Op::AllocLocals); + self.emit_u32(total_slots as u32); + } + + self.emit_scope_thunks(thunks); + self.emit_expr(*body); + self.emit_op(Op::Return); + + self.pop_scope(); + } + _ => { + self.push_scope(false, None, &[]); + self.emit_expr(ir); + self.emit_op(Op::Return); + self.pop_scope(); + } + } + } + + fn emit_toplevel_scoped(&mut self, ir: RawIrRef<'_>) { + match ir.deref() { + Ir::TopLevel { body, thunks } => { + let with_thunk_count = self.count_with_thunks(*body); + let total_slots = thunks.len() + with_thunk_count; + + let all_thunks = self.collect_all_thunks(thunks, *body); + let thunk_ids: Vec = all_thunks.iter().map(|&(id, _)| id).collect(); + + self.push_scope(false, None, &thunk_ids); + + if total_slots > 0 { + self.emit_op(Op::AllocLocals); + self.emit_u32(total_slots as u32); + } + + self.emit_scope_thunks(thunks); + self.emit_expr(*body); + self.emit_op(Op::Return); + + self.pop_scope(); + } + _ => { + self.push_scope(false, None, &[]); + self.emit_expr(ir); + self.emit_op(Op::Return); + self.pop_scope(); + } + } + } + + fn emit_scope_thunks(&mut self, thunks: &[(ThunkId, RawIrRef<'_>)]) { + for &(id, inner) in thunks { + let label = format!("e{}", id.0); + let label_idx = self.ctx.intern_string(&label); + + let skip_patch = self.emit_jump_placeholder(); + let entry_point = self.code.len() as u32; + self.emit_expr(inner); + self.emit_op(Op::Return); + self.patch_jump_target(skip_patch); + self.emit_op(Op::MakeThunk); + self.emit_u32(entry_point); + self.emit_u32(label_idx); + let (_, local_idx) = self.resolve_thunk(id); + self.emit_op(Op::StoreLocal); + self.emit_u32(local_idx); + } + } + + fn emit_expr(&mut self, ir: RawIrRef<'_>) { + match ir.deref() { + &Ir::Int(x) => { + let idx = self.ctx.intern_constant(Constant::Int(x)); + self.emit_op(Op::PushConst); + self.emit_u32(idx); + } + &Ir::Float(x) => { + let idx = self.ctx.intern_constant(Constant::Float(x.to_bits())); + self.emit_op(Op::PushConst); + self.emit_u32(idx); + } + &Ir::Bool(true) => self.emit_op(Op::PushTrue), + &Ir::Bool(false) => self.emit_op(Op::PushFalse), + Ir::Null => self.emit_op(Op::PushNull), + Ir::Str(s) => { + let idx = self.ctx.intern_string(s.deref()); + self.emit_op(Op::PushString); + self.emit_u32(idx); + } + &Ir::Path(p) => { + self.emit_expr(p); + self.emit_op(Op::ResolvePath); + } + &Ir::If { cond, consq, alter } => { + self.emit_expr(cond); + self.emit_op(Op::ForceBool); + + self.emit_op(Op::JumpIfFalse); + let else_placeholder = self.emit_i32_placeholder(); + let after_jif = self.code.len(); + + self.emit_expr(consq); + + self.emit_op(Op::Jump); + let end_placeholder = self.emit_i32_placeholder(); + let after_jump = self.code.len(); + + let else_offset = (after_jump as i32) - (after_jif as i32); + self.patch_i32(else_placeholder, else_offset); + + self.emit_expr(alter); + + let end_offset = (self.code.len() as i32) - (after_jump as i32); + self.patch_i32(end_placeholder, end_offset); + } + &Ir::BinOp { lhs, rhs, kind } => { + self.emit_binop(lhs, rhs, kind); + } + &Ir::UnOp { rhs, kind } => match kind { + UnOpKind::Neg => { + self.emit_expr(rhs); + self.emit_op(Op::OpNeg); + } + UnOpKind::Not => { + self.emit_expr(rhs); + self.emit_op(Op::OpNot); + } + }, + &Ir::Func { + body, + ref param, + arg, + ref thunks, + } => { + self.emit_func(arg, thunks, param, body); + } + Ir::AttrSet { stcs, dyns } => { + self.emit_attrset(stcs, dyns); + } + Ir::List { items } => { + for &item in items.iter() { + self.emit_expr(item); + } + self.emit_op(Op::MakeList); + self.emit_u32(items.len() as u32); + } + &Ir::Call { func, arg, span } => { + self.emit_expr(func); + self.emit_expr(arg); + let span_id = self.ctx.register_span(span); + self.emit_op(Op::Call); + self.emit_u32(span_id); + } + &Ir::Arg(id) => { + let (layer, local) = self.resolve_arg(id); + self.emit_load(layer, local); + } + &Ir::TopLevel { body, ref thunks } => { + self.emit_toplevel_inner(body, thunks); + } + &Ir::Select { + expr, + ref attrpath, + default, + span, + } => { + self.emit_select(expr, attrpath, default, span); + } + &Ir::Thunk(id) => { + let (layer, local) = self.resolve_thunk(id); + self.emit_load(layer, local); + } + Ir::Builtins => { + self.emit_op(Op::LoadBuiltins); + } + &Ir::Builtin(name) => { + let sym = self.ctx.get_sym(name).to_string(); + let idx = self.ctx.intern_string(&sym); + self.emit_op(Op::LoadBuiltin); + self.emit_u32(idx); + } + &Ir::ConcatStrings { + ref parts, + force_string, + } => { + for &part in parts.iter() { + self.emit_expr(part); + } + self.emit_op(Op::ConcatStrings); + self.emit_u16(parts.len() as u16); + self.emit_u8(if force_string { 1 } else { 0 }); + } + &Ir::HasAttr { lhs, ref rhs } => { + self.emit_has_attr(lhs, rhs); + } + Ir::Assert { + assertion, + expr, + assertion_raw, + span, + } => { + let raw_idx = self.ctx.intern_string(assertion_raw); + let span_id = self.ctx.register_span(*span); + self.emit_expr(*assertion); + self.emit_expr(*expr); + self.emit_op(Op::Assert); + self.emit_u32(raw_idx); + self.emit_u32(span_id); + } + &Ir::CurPos(span) => { + let span_id = self.ctx.register_span(span); + self.emit_op(Op::MkPos); + self.emit_u32(span_id); + } + &Ir::ReplBinding(name) => { + let sym = self.ctx.get_sym(name).to_string(); + let idx = self.ctx.intern_string(&sym); + self.emit_op(Op::LoadReplBinding); + self.emit_u32(idx); + } + &Ir::ScopedImportBinding(name) => { + let sym = self.ctx.get_sym(name).to_string(); + let idx = self.ctx.intern_string(&sym); + self.emit_op(Op::LoadScopedBinding); + self.emit_u32(idx); + } + &Ir::With { + namespace, + body, + ref thunks, + } => { + self.emit_with(namespace, body, thunks); + } + &Ir::WithLookup(name) => { + let sym = self.ctx.get_sym(name).to_string(); + let idx = self.ctx.intern_string(&sym); + self.emit_op(Op::WithLookup); + self.emit_u32(idx); + } + } + } + + fn emit_binop(&mut self, lhs: RawIrRef<'_>, rhs: RawIrRef<'_>, kind: BinOpKind) { + use BinOpKind::*; + match kind { + And => { + self.emit_expr(lhs); + self.emit_op(Op::ForceBool); + self.emit_op(Op::JumpIfFalse); + let skip_placeholder = self.emit_i32_placeholder(); + let after_jif = self.code.len(); + + self.emit_expr(rhs); + self.emit_op(Op::ForceBool); + self.emit_op(Op::Jump); + let end_placeholder = self.emit_i32_placeholder(); + let after_jump = self.code.len(); + + let false_offset = (after_jump as i32) - (after_jif as i32); + self.patch_i32(skip_placeholder, false_offset); + + self.emit_op(Op::PushFalse); + + let end_offset = (self.code.len() as i32) - (after_jump as i32); + self.patch_i32(end_placeholder, end_offset); + } + Or => { + self.emit_expr(lhs); + self.emit_op(Op::ForceBool); + self.emit_op(Op::JumpIfTrue); + let skip_placeholder = self.emit_i32_placeholder(); + let after_jit = self.code.len(); + + self.emit_expr(rhs); + self.emit_op(Op::ForceBool); + self.emit_op(Op::Jump); + let end_placeholder = self.emit_i32_placeholder(); + let after_jump = self.code.len(); + + let true_offset = (after_jump as i32) - (after_jit as i32); + self.patch_i32(skip_placeholder, true_offset); + + self.emit_op(Op::PushTrue); + + let end_offset = (self.code.len() as i32) - (after_jump as i32); + self.patch_i32(end_placeholder, end_offset); + } + Impl => { + self.emit_expr(lhs); + self.emit_op(Op::ForceBool); + self.emit_op(Op::JumpIfFalse); + let skip_placeholder = self.emit_i32_placeholder(); + let after_jif = self.code.len(); + + self.emit_expr(rhs); + self.emit_op(Op::ForceBool); + self.emit_op(Op::Jump); + let end_placeholder = self.emit_i32_placeholder(); + let after_jump = self.code.len(); + + let true_offset = (after_jump as i32) - (after_jif as i32); + self.patch_i32(skip_placeholder, true_offset); + + self.emit_op(Op::PushTrue); + + let end_offset = (self.code.len() as i32) - (after_jump as i32); + self.patch_i32(end_placeholder, end_offset); + } + PipeL => { + self.emit_expr(rhs); + self.emit_expr(lhs); + self.emit_op(Op::CallNoSpan); + } + PipeR => { + self.emit_expr(lhs); + self.emit_expr(rhs); + self.emit_op(Op::CallNoSpan); + } + _ => { + self.emit_expr(lhs); + self.emit_expr(rhs); + self.emit_op(match kind { + Add => Op::OpAdd, + Sub => Op::OpSub, + Mul => Op::OpMul, + Div => Op::OpDiv, + Eq => Op::OpEq, + Neq => Op::OpNeq, + Lt => Op::OpLt, + Gt => Op::OpGt, + Leq => Op::OpLeq, + Geq => Op::OpGeq, + Con => Op::OpConcat, + Upd => Op::OpUpdate, + _ => unreachable!(), + }); + } + } + } + + fn emit_func( + &mut self, + arg: ArgId, + thunks: &[(ThunkId, RawIrRef<'_>)], + param: &Option>, + body: RawIrRef<'_>, + ) { + let with_thunk_count = self.count_with_thunks(body); + let total_slots = thunks.len() + with_thunk_count; + + let all_thunks = self.collect_all_thunks(thunks, body); + let thunk_ids: Vec = all_thunks.iter().map(|&(id, _)| id).collect(); + + let skip_patch = self.emit_jump_placeholder(); + let entry_point = self.code.len() as u32; + self.push_scope(true, Some(arg), &thunk_ids); + self.emit_scope_thunks(thunks); + self.emit_expr(body); + self.emit_op(Op::Return); + self.pop_scope(); + self.patch_jump_target(skip_patch); + + if let Some(Param { + required, + optional, + ellipsis, + }) = param + { + self.emit_op(Op::MakePatternClosure); + self.emit_u32(entry_point); + self.emit_u32(total_slots as u32); + self.emit_u16(required.len() as u16); + self.emit_u16(optional.len() as u16); + self.emit_u8(if *ellipsis { 1 } else { 0 }); + + for &(sym, _) in required.iter() { + let name = self.ctx.get_sym(sym).to_string(); + let idx = self.ctx.intern_string(&name); + self.emit_u32(idx); + } + for &(sym, _) in optional.iter() { + let name = self.ctx.get_sym(sym).to_string(); + let idx = self.ctx.intern_string(&name); + self.emit_u32(idx); + } + for &(sym, span) in required.iter().chain(optional.iter()) { + let name = self.ctx.get_sym(sym).to_string(); + let name_idx = self.ctx.intern_string(&name); + let span_id = self.ctx.register_span(span); + self.emit_u32(name_idx); + self.emit_u32(span_id); + } + } else { + self.emit_op(Op::MakeClosure); + self.emit_u32(entry_point); + self.emit_u32(total_slots as u32); + } + } + + fn emit_attrset( + &mut self, + stcs: &crate::ir::HashMap<'_, SymId, (RawIrRef<'_>, TextRange)>, + dyns: &[(RawIrRef<'_>, RawIrRef<'_>, TextRange)], + ) { + if stcs.is_empty() && dyns.is_empty() { + self.emit_op(Op::MakeEmptyAttrs); + return; + } + + if !dyns.is_empty() { + for (&sym, &(val, _)) in stcs.iter() { + let key = self.ctx.get_sym(sym).to_string(); + let idx = self.ctx.intern_string(&key); + self.emit_op(Op::PushString); + self.emit_u32(idx); + self.emit_expr(val); + } + for (_, &(_, span)) in stcs.iter() { + let span_id = self.ctx.register_span(span); + let idx = self.ctx.intern_constant(Constant::Int(span_id as i64)); + self.emit_op(Op::PushConst); + self.emit_u32(idx); + } + for &(key, val, span) in dyns.iter() { + self.emit_expr(key); + self.emit_expr(val); + let span_id = self.ctx.register_span(span); + let idx = self.ctx.intern_constant(Constant::Int(span_id as i64)); + self.emit_op(Op::PushConst); + self.emit_u32(idx); + } + self.emit_op(Op::MakeAttrsDyn); + self.emit_u32(stcs.len() as u32); + self.emit_u32(dyns.len() as u32); + } else { + for (&sym, &(val, _)) in stcs.iter() { + let key = self.ctx.get_sym(sym).to_string(); + let idx = self.ctx.intern_string(&key); + self.emit_op(Op::PushString); + self.emit_u32(idx); + self.emit_expr(val); + } + for (_, &(_, span)) in stcs.iter() { + let span_id = self.ctx.register_span(span); + let idx = self.ctx.intern_constant(Constant::Int(span_id as i64)); + self.emit_op(Op::PushConst); + self.emit_u32(idx); + } + self.emit_op(Op::MakeAttrs); + self.emit_u32(stcs.len() as u32); + } + } + + fn emit_select( + &mut self, + expr: RawIrRef<'_>, + attrpath: &[Attr>], + default: Option>, + span: TextRange, + ) { + self.emit_expr(expr); + for attr in attrpath.iter() { + match attr { + Attr::Str(sym, _) => { + let key = self.ctx.get_sym(*sym).to_string(); + let idx = self.ctx.intern_string(&key); + self.emit_op(Op::PushString); + self.emit_u32(idx); + } + Attr::Dynamic(expr, _) => { + self.emit_expr(*expr); + } + } + } + + if let Some(default) = default { + self.emit_expr(default); + let span_id = self.ctx.register_span(span); + self.emit_op(Op::SelectDefault); + self.emit_u16(attrpath.len() as u16); + self.emit_u32(span_id); + } else { + let span_id = self.ctx.register_span(span); + self.emit_op(Op::Select); + self.emit_u16(attrpath.len() as u16); + self.emit_u32(span_id); + } + } + + fn emit_has_attr(&mut self, lhs: RawIrRef<'_>, rhs: &[Attr>]) { + self.emit_expr(lhs); + for attr in rhs.iter() { + match attr { + Attr::Str(sym, _) => { + let key = self.ctx.get_sym(*sym).to_string(); + let idx = self.ctx.intern_string(&key); + self.emit_op(Op::PushString); + self.emit_u32(idx); + } + Attr::Dynamic(expr, _) => { + self.emit_expr(*expr); + } + } + } + self.emit_op(Op::HasAttr); + self.emit_u16(rhs.len() as u16); + } + + fn emit_with( + &mut self, + namespace: RawIrRef<'_>, + body: RawIrRef<'_>, + thunks: &[(ThunkId, RawIrRef<'_>)], + ) { + self.emit_expr(namespace); + self.emit_op(Op::PushWith); + self.emit_scope_thunks(thunks); + self.emit_expr(body); + self.emit_op(Op::PopWith); + } + + fn emit_toplevel_inner(&mut self, body: RawIrRef<'_>, thunks: &[(ThunkId, RawIrRef<'_>)]) { + self.emit_scope_thunks(thunks); + self.emit_expr(body); + } +} diff --git a/nix-js/src/context.rs b/nix-js/src/context.rs index 76acc33..447c686 100644 --- a/nix-js/src/context.rs +++ b/nix-js/src/context.rs @@ -8,13 +8,15 @@ use hashbrown::{DefaultHashBuilder, HashMap, HashSet, HashTable}; use rnix::TextRange; use string_interner::DefaultStringInterner; +use crate::bytecode::{self, BytecodeContext, Bytecode, Constant}; use crate::codegen::{CodegenContext, compile}; +use crate::disassembler::{Disassembler, DisassemblerContext}; use crate::downgrade::*; use crate::error::{Error, Result, Source}; use crate::ir::{ArgId, Ir, IrKey, IrRef, RawIrRef, SymId, ThunkId, ir_content_eq}; #[cfg(feature = "inspector")] use crate::runtime::inspector::InspectorServer; -use crate::runtime::{Runtime, RuntimeContext}; +use crate::runtime::{ForceMode, Runtime, RuntimeContext}; use crate::store::{DaemonStore, Store, StoreConfig}; use crate::value::{Symbol, Value}; @@ -53,16 +55,16 @@ pub struct Context { _inspector_server: Option, } -macro_rules! eval { - ($name:ident, $wrapper:literal) => { +macro_rules! eval_bc { + ($name:ident, $mode:expr) => { pub fn $name(&mut self, source: Source) -> Result { tracing::info!("Starting evaluation"); - tracing::debug!("Compiling code"); - let code = self.compile(source)?; + tracing::debug!("Compiling bytecode"); + let bytecode = self.ctx.compile_bytecode(source)?; - tracing::debug!("Executing JavaScript"); - self.runtime.eval(format!($wrapper, code), &mut self.ctx) + tracing::debug!("Executing bytecode"); + self.runtime.eval_bytecode(bytecode, &mut self.ctx, $mode) } }; } @@ -137,10 +139,9 @@ impl Context { Ok(()) } - eval!(eval, "Nix.force({})"); - eval!(eval_shallow, "Nix.forceShallow({})"); - eval!(eval_deep, "Nix.forceDeep({})"); - + eval_bc!(eval, ForceMode::Force); + eval_bc!(eval_shallow, ForceMode::ForceShallow); + eval_bc!(eval_deep, ForceMode::ForceDeep); pub fn eval_repl<'a>(&'a mut self, source: Source, scope: &'a HashSet) -> Result { tracing::info!("Starting evaluation"); @@ -156,6 +157,18 @@ impl Context { self.ctx.compile(source, None) } + pub fn compile_bytecode(&mut self, source: Source) -> Result { + self.ctx.compile_bytecode(source) + } + + pub fn disassemble(&self, bytecode: &Bytecode) -> String { + Disassembler::new(bytecode, &self.ctx).disassemble() + } + + pub fn disassemble_colored(&self, bytecode: &Bytecode) -> String { + Disassembler::new(bytecode, &self.ctx).disassemble_colored() + } + pub fn get_store_dir(&self) -> &str { self.ctx.get_store_dir() } @@ -188,6 +201,12 @@ struct Ctx { store: DaemonStore, spans: UnsafeCell>, thunk_count: usize, + global_strings: Vec, + global_string_map: HashMap, + global_constants: Vec, + global_constant_map: HashMap, + synced_strings: usize, + synced_constants: usize, } /// Owns the bump allocator and a read-only reference into it. @@ -261,6 +280,12 @@ impl Ctx { store, spans: UnsafeCell::new(Vec::new()), thunk_count: 0, + global_strings: Vec::new(), + global_string_map: HashMap::new(), + global_constants: Vec::new(), + global_constant_map: HashMap::new(), + synced_strings: 0, + synced_constants: 0, }) } @@ -349,6 +374,30 @@ impl Ctx { tracing::debug!("Generated scoped code: {}", &code); Ok(code) } + + fn compile_bytecode(&mut self, source: Source) -> Result { + let root = self.downgrade(source, None)?; + tracing::debug!("Generating bytecode"); + let bytecode = bytecode::compile_bytecode(root.as_ref(), self); + tracing::debug!("Compiled bytecode: {:#04X?}", bytecode.code); + Ok(bytecode) + } + + fn compile_bytecode_scoped( + &mut self, + source: Source, + scope: Vec, + ) -> Result { + let scope = Scope::ScopedImport( + scope + .into_iter() + .map(|k| self.symbols.get_or_intern(k)) + .collect(), + ); + let root = self.downgrade(source, Some(scope))?; + tracing::debug!("Generating bytecode for scoped import"); + Ok(bytecode::compile_bytecode_scoped(root.as_ref(), self)) + } } impl CodegenContext for Ctx { @@ -378,6 +427,40 @@ impl CodegenContext for Ctx { } } +impl BytecodeContext for Ctx { + fn intern_string(&mut self, s: &str) -> u32 { + if let Some(&idx) = self.global_string_map.get(s) { + return idx; + } + let idx = self.global_strings.len() as u32; + self.global_strings.push(s.to_string()); + self.global_string_map.insert(s.to_string(), idx); + idx + } + + fn intern_constant(&mut self, c: Constant) -> u32 { + if let Some(&idx) = self.global_constant_map.get(&c) { + return idx; + } + let idx = self.global_constants.len() as u32; + self.global_constants.push(c.clone()); + self.global_constant_map.insert(c, idx); + idx + } + + fn register_span(&self, range: TextRange) -> u32 { + CodegenContext::register_span(self, range) as u32 + } + + fn get_sym(&self, id: SymId) -> &str { + self.symbols.resolve(id).expect("SymId out of bounds") + } + + fn get_current_dir(&self) -> &Path { + Ctx::get_current_dir(self) + } +} + impl RuntimeContext for Ctx { fn get_current_dir(&self) -> &Path { self.get_current_dir() @@ -391,6 +474,16 @@ impl RuntimeContext for Ctx { fn compile_scoped(&mut self, source: Source, scope: Vec) -> Result { self.compile_scoped(source, scope) } + fn compile_bytecode(&mut self, source: Source) -> Result { + self.compile_bytecode(source) + } + fn compile_bytecode_scoped( + &mut self, + source: Source, + scope: Vec, + ) -> Result { + self.compile_bytecode_scoped(source, scope) + } fn get_source(&self, id: usize) -> Source { self.sources.get(id).expect("source not found").clone() } @@ -401,6 +494,24 @@ impl RuntimeContext for Ctx { let spans = unsafe { &*self.spans.get() }; spans[id] } + fn take_unsynced(&mut self) -> (Vec, Vec, usize, usize) { + let strings_base = self.synced_strings; + let constants_base = self.synced_constants; + let new_strings = self.global_strings[strings_base..].to_vec(); + let new_constants = self.global_constants[constants_base..].to_vec(); + self.synced_strings = self.global_strings.len(); + self.synced_constants = self.global_constants.len(); + (new_strings, new_constants, strings_base, constants_base) + } +} + +impl DisassemblerContext for Ctx { + fn lookup_string(&self, id: u32) -> &str { + self.global_strings.get(id as usize).expect("string not found") + } + fn lookup_constant(&self, id: u32) -> &Constant { + self.global_constants.get(id as usize).expect("constant not found") + } } enum Scope<'ctx> { diff --git a/nix-js/src/disassembler.rs b/nix-js/src/disassembler.rs new file mode 100644 index 0000000..dc769e0 --- /dev/null +++ b/nix-js/src/disassembler.rs @@ -0,0 +1,354 @@ +use std::fmt::Write; + +use colored::Colorize; +use num_enum::TryFromPrimitive; + +use crate::bytecode::{Bytecode, Constant, Op}; + +pub(crate) trait DisassemblerContext { + fn lookup_string(&self, id: u32) -> &str; + fn lookup_constant(&self, id: u32) -> &Constant; +} + +pub(crate) struct Disassembler<'a, Ctx> { + code: &'a [u8], + ctx: &'a Ctx, + pos: usize, +} + +impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> { + pub fn new(bytecode: &'a Bytecode, ctx: &'a Ctx) -> Self { + Self { + code: &bytecode.code, + ctx, + pos: 0, + } + } + + fn read_u8(&mut self) -> u8 { + let b = self.code[self.pos]; + self.pos += 1; + b + } + + fn read_u16(&mut self) -> u16 { + let bytes = self.code[self.pos..self.pos + 2] + .try_into() + .expect("no enough bytes"); + self.pos += 2; + u16::from_le_bytes(bytes) + } + + fn read_u32(&mut self) -> u32 { + let bytes = self.code[self.pos..self.pos + 4] + .try_into() + .expect("no enough bytes"); + self.pos += 4; + u32::from_le_bytes(bytes) + } + + fn read_i32(&mut self) -> i32 { + let bytes = self.code[self.pos..self.pos + 4] + .try_into() + .expect("no enough bytes"); + self.pos += 4; + i32::from_le_bytes(bytes) + } + + pub fn disassemble(&mut self) -> String { + self.disassemble_impl(false) + } + + pub fn disassemble_colored(&mut self) -> String { + self.disassemble_impl(true) + } + + fn disassemble_impl(&mut self, color: bool) -> String { + let mut out = String::new(); + if color { + let _ = writeln!(out, "{}", "=== Bytecode Disassembly ===".bold().white()); + let _ = writeln!( + out, + "{} {}", + "Length:".white(), + format!("{} bytes", self.code.len()).cyan() + ); + } else { + let _ = writeln!(out, "=== Bytecode Disassembly ==="); + let _ = writeln!(out, "Length: {} bytes", self.code.len()); + } + + while self.pos < self.code.len() { + let start_pos = self.pos; + let op_byte = self.read_u8(); + let (mnemonic, args) = self.decode_instruction(op_byte, start_pos); + + let bytes_slice = &self.code[start_pos + 1..self.pos]; + + for (i, chunk) in bytes_slice.chunks(4).enumerate() { + let bytes_str = { + let mut temp = String::new(); + if i == 0 { + let _ = write!(&mut temp, "{:02x}", self.code[start_pos]); + } else { + let _ = write!(&mut temp, " "); + } + for b in chunk.iter() { + let _ = write!(&mut temp, " {:02x}", b); + } + temp + }; + + if i == 0 { + if color { + let sep = if args.is_empty() { "" } else { " " }; + let _ = writeln!( + out, + "{} {:<14} | {}{}{}", + format!("{:04x}", start_pos).dimmed(), + bytes_str.green(), + mnemonic.yellow().bold(), + sep, + args.cyan() + ); + } else { + let op_str = if args.is_empty() { + mnemonic.to_string() + } else { + format!("{} {}", mnemonic, args) + }; + let _ = writeln!(out, "{:04x} {:<14} | {}", start_pos, bytes_str, op_str); + } + } else { + let extra_width = start_pos.ilog2() >> 4; + if color { + let _ = write!(out, " "); + for _ in 0..extra_width { + let _ = write!(out, " "); + } + let _ = writeln!(out, " {:<14} |", bytes_str.green()); + } else { + let _ = write!(out, " "); + for _ in 0..extra_width { + let _ = write!(out, " "); + } + let _ = writeln!(out, " {:<14} |", bytes_str); + } + } + } + } + out + } + + fn decode_instruction(&mut self, op_byte: u8, current_pc: usize) -> (&'static str, String) { + let op = Op::try_from_primitive(op_byte).expect("invalid op code"); + + match op { + Op::PushConst => { + let idx = self.read_u32(); + let val = self.ctx.lookup_constant(idx); + let val_str = match val { + Constant::Int(i) => format!("Int({})", i), + Constant::Float(f) => format!("Float(bits: {})", f), + }; + ("PushConst", format!("@{} ({})", idx, val_str)) + } + Op::PushString => { + let idx = self.read_u32(); + let s = self.ctx.lookup_string(idx); + let len = s.len(); + let mut s_fmt = format!("{:?}", s); + if s_fmt.len() > 60 { + s_fmt.truncate(57); + #[allow(clippy::unwrap_used)] + write!(s_fmt, "...\" (total {len} bytes)").unwrap(); + } + ("PushString", format!("@{} {}", idx, s_fmt)) + } + Op::PushNull => ("PushNull", String::new()), + Op::PushTrue => ("PushTrue", String::new()), + Op::PushFalse => ("PushFalse", String::new()), + + Op::LoadLocal => { + let idx = self.read_u32(); + ("LoadLocal", format!("[{}]", idx)) + } + Op::LoadOuter => { + let depth = self.read_u8(); + let idx = self.read_u32(); + ("LoadOuter", format!("depth={} [{}]", depth, idx)) + } + Op::StoreLocal => { + let idx = self.read_u32(); + ("StoreLocal", format!("[{}]", idx)) + } + Op::AllocLocals => { + let count = self.read_u32(); + ("AllocLocals", format!("count={}", count)) + } + + Op::MakeThunk => { + let offset = self.read_u32(); + let label_idx = self.read_u32(); + let label = self.ctx.lookup_string(label_idx); + ("MakeThunk", format!("-> {:04x} label={}", offset, label)) + } + Op::MakeClosure => { + let offset = self.read_u32(); + let slots = self.read_u32(); + ("MakeClosure", format!("-> {:04x} slots={}", offset, slots)) + } + Op::MakePatternClosure => { + let offset = self.read_u32(); + let slots = self.read_u32(); + let req_count = self.read_u16(); + let opt_count = self.read_u16(); + let ellipsis = self.read_u8() != 0; + + let mut arg_str = format!( + "-> {:04x} slots={} req={} opt={} ...={})", + offset, slots, req_count, opt_count, ellipsis + ); + + arg_str.push_str(" Args=["); + for _ in 0..req_count { + let idx = self.read_u32(); + arg_str.push_str(&format!("Req({}) ", self.ctx.lookup_string(idx))); + } + for _ in 0..opt_count { + let idx = self.read_u32(); + arg_str.push_str(&format!("Opt({}) ", self.ctx.lookup_string(idx))); + } + + let total_args = req_count + opt_count; + for _ in 0..total_args { + let _name_idx = self.read_u32(); + let _span_id = self.read_u32(); + } + arg_str.push(']'); + + ("MakePatternClosure", arg_str) + } + + Op::Call => { + let span_id = self.read_u32(); + ("Call", format!("span={}", span_id)) + } + Op::CallNoSpan => ("CallNoSpan", String::new()), + + Op::MakeAttrs => { + let count = self.read_u32(); + ("MakeAttrs", format!("size={}", count)) + } + Op::MakeAttrsDyn => { + let static_count = self.read_u32(); + let dyn_count = self.read_u32(); + ( + "MakeAttrsDyn", + format!("static={} dyn={}", static_count, dyn_count), + ) + } + Op::MakeEmptyAttrs => ("MakeEmptyAttrs", String::new()), + + Op::Select => { + let path_len = self.read_u16(); + let span_id = self.read_u32(); + ("Select", format!("path_len={} span={}", path_len, span_id)) + } + Op::SelectDefault => { + let path_len = self.read_u16(); + let span_id = self.read_u32(); + ( + "SelectDefault", + format!("path_len={} span={}", path_len, span_id), + ) + } + Op::HasAttr => { + let path_len = self.read_u16(); + ("HasAttr", format!("path_len={}", path_len)) + } + + Op::MakeList => { + let count = self.read_u32(); + ("MakeList", format!("size={}", count)) + } + + Op::OpAdd => ("OpAdd", String::new()), + Op::OpSub => ("OpSub", String::new()), + Op::OpMul => ("OpMul", String::new()), + Op::OpDiv => ("OpDiv", String::new()), + Op::OpEq => ("OpEq", String::new()), + Op::OpNeq => ("OpNeq", String::new()), + Op::OpLt => ("OpLt", String::new()), + Op::OpGt => ("OpGt", String::new()), + Op::OpLeq => ("OpLeq", String::new()), + Op::OpGeq => ("OpGeq", String::new()), + Op::OpConcat => ("OpConcat", String::new()), + Op::OpUpdate => ("OpUpdate", String::new()), + Op::OpNeg => ("OpNeg", String::new()), + Op::OpNot => ("OpNot", String::new()), + + Op::ForceBool => ("ForceBool", String::new()), + + Op::JumpIfFalse => { + let offset = self.read_i32(); + let target = (current_pc as isize + 1 + 4 + offset as isize) as usize; + ( + "JumpIfFalse", + format!("-> {:04x} offset={}", target, offset), + ) + } + Op::JumpIfTrue => { + let offset = self.read_i32(); + let target = (current_pc as isize + 1 + 4 + offset as isize) as usize; + ("JumpIfTrue", format!("-> {:04x} offset={}", target, offset)) + } + Op::Jump => { + let offset = self.read_i32(); + let target = (current_pc as isize + 1 + 4 + offset as isize) as usize; + ("Jump", format!("-> {:04x} offset={}", target, offset)) + } + + Op::ConcatStrings => { + let count = self.read_u16(); + let force = self.read_u8(); + ("ConcatStrings", format!("count={} force={}", count, force)) + } + Op::ResolvePath => ("ResolvePath", String::new()), + Op::Assert => { + let raw_idx = self.read_u32(); + let span_id = self.read_u32(); + ("Assert", format!("text_id={} span={}", raw_idx, span_id)) + } + Op::PushWith => ("PushWith", String::new()), + Op::PopWith => ("PopWith", String::new()), + Op::WithLookup => { + let idx = self.read_u32(); + let name = self.ctx.lookup_string(idx); + ("WithLookup", format!("{:?}", name)) + } + + Op::LoadBuiltins => ("LoadBuiltins", String::new()), + Op::LoadBuiltin => { + let idx = self.read_u32(); + let name = self.ctx.lookup_string(idx); + ("LoadBuiltin", format!("{:?}", name)) + } + Op::MkPos => { + let span_id = self.read_u32(); + ("MkPos", format!("id={}", span_id)) + } + Op::LoadReplBinding => { + let idx = self.read_u32(); + let name = self.ctx.lookup_string(idx); + ("LoadReplBinding", format!("{:?}", name)) + } + Op::LoadScopedBinding => { + let idx = self.read_u32(); + let name = self.ctx.lookup_string(idx); + ("LoadScopedBinding", format!("{:?}", name)) + } + Op::Return => ("Return", String::new()), + } + } +} diff --git a/nix-js/src/lib.rs b/nix-js/src/lib.rs index d22368a..6e2ac4c 100644 --- a/nix-js/src/lib.rs +++ b/nix-js/src/lib.rs @@ -5,8 +5,10 @@ pub mod error; pub mod logging; pub mod value; +mod bytecode; mod codegen; mod derivation; +mod disassembler; mod downgrade; mod fetcher; mod ir; diff --git a/nix-js/src/main.rs b/nix-js/src/main.rs index a054925..b31a2ab 100644 --- a/nix-js/src/main.rs +++ b/nix-js/src/main.rs @@ -77,10 +77,10 @@ fn run_compile(context: &mut Context, src: ExprSource, silent: bool) -> Result<( } else { unreachable!() }; - match context.compile(src) { + match context.compile_bytecode(src) { Ok(compiled) => { if !silent { - println!("{compiled}"); + println!("{}", context.disassemble_colored(&compiled)); } } Err(err) => { diff --git a/nix-js/src/runtime.rs b/nix-js/src/runtime.rs index ab0e4e5..7a878e9 100644 --- a/nix-js/src/runtime.rs +++ b/nix-js/src/runtime.rs @@ -6,6 +6,7 @@ use std::path::Path; use deno_core::PollEventLoopOptions; use deno_core::{Extension, ExtensionFileSource, JsRuntime, OpState, RuntimeOptions, v8}; +use crate::bytecode::{Bytecode, Constant}; use crate::error::{Error, Result, Source}; use crate::store::DaemonStore; use crate::value::{AttrSet, List, Symbol, Value}; @@ -24,9 +25,16 @@ pub(crate) trait RuntimeContext: 'static { fn add_source(&mut self, path: Source); fn compile(&mut self, source: Source) -> Result; fn compile_scoped(&mut self, source: Source, scope: Vec) -> Result; + fn compile_bytecode(&mut self, source: Source) -> Result; + fn compile_bytecode_scoped( + &mut self, + source: Source, + scope: Vec, + ) -> Result; fn get_source(&self, id: usize) -> Source; fn get_store(&self) -> &DaemonStore; fn get_span(&self, id: usize) -> (usize, rnix::TextRange); + fn take_unsynced(&mut self) -> (Vec, Vec, usize, usize); } pub(crate) trait OpStateExt { @@ -121,6 +129,7 @@ pub(crate) struct Runtime { #[cfg(feature = "inspector")] wait_for_inspector: bool, symbols: GlobalSymbols, + cached_fns: CachedFunctions, _marker: PhantomData, } @@ -162,9 +171,11 @@ impl Runtime { js_runtime.op_state().borrow_mut().put(RegexCache::new()); js_runtime.op_state().borrow_mut().put(DrvHashCache::new()); - let symbols = { + let (symbols, cached_fns) = { deno_core::scope!(scope, &mut js_runtime); - Self::get_symbols(scope)? + let symbols = Self::get_symbols(scope)?; + let cached_fns = Self::get_cached_functions(scope)?; + (symbols, cached_fns) }; Ok(Self { @@ -177,6 +188,7 @@ impl Runtime { #[cfg(feature = "inspector")] wait_for_inspector: inspector_options.wait, symbols, + cached_fns, _marker: PhantomData, }) } @@ -227,6 +239,87 @@ impl Runtime { Ok(to_value(local_value, scope, symbols)) } + pub(crate) fn eval_bytecode( + &mut self, + result: Bytecode, + ctx: &mut Ctx, + force_mode: ForceMode, + ) -> Result { + let ctx: &'static mut Ctx = unsafe { &mut *(ctx as *mut Ctx) }; + { + deno_core::scope!(scope, self.js_runtime); + sync_global_tables(scope, &self.cached_fns, ctx); + } + let op_state = self.js_runtime.op_state(); + op_state.borrow_mut().put(ctx); + + #[cfg(feature = "inspector")] + if self.wait_for_inspector { + self.js_runtime + .inspector() + .wait_for_session_and_break_on_next_statement(); + } else { + self.js_runtime.inspector().wait_for_session(); + } + + deno_core::scope!(scope, self.js_runtime); + + let store = v8::ArrayBuffer::new_backing_store_from_boxed_slice(result.code); + let ab = v8::ArrayBuffer::with_backing_store(scope, &store.make_shared()); + let u8a = v8::Uint8Array::new(scope, ab, 0, ab.byte_length()) + .ok_or_else(|| Error::internal("failed to create Uint8Array".into()))?; + + let dir = v8::String::new(scope, &result.current_dir) + .ok_or_else(|| Error::internal("failed to create dir string".into()))?; + + let undef = v8::undefined(scope); + let tc = std::pin::pin!(v8::TryCatch::new(scope)); + let scope = &mut tc.init(); + + let exec_bytecode = v8::Local::new(scope, &self.cached_fns.exec_bytecode); + let raw_result = exec_bytecode + .call(scope, undef.into(), &[u8a.into(), dir.into()]) + .ok_or_else(|| { + scope + .exception() + .map(|e| { + let op_state_borrow = op_state.borrow(); + let ctx: &Ctx = op_state_borrow.get_ctx(); + Box::new(crate::error::parse_js_error( + deno_core::error::JsError::from_v8_exception(scope, e), + ctx, + )) + }) + .unwrap_or_else(|| Error::internal("bytecode execution failed".into())) + })?; + + let force_fn = match force_mode { + ForceMode::Force => &self.cached_fns.force_fn, + ForceMode::ForceShallow => &self.cached_fns.force_shallow_fn, + ForceMode::ForceDeep => &self.cached_fns.force_deep_fn, + }; + let force_fn = v8::Local::new(scope, force_fn); + + let forced = force_fn + .call(scope, undef.into(), &[raw_result]) + .ok_or_else(|| { + scope + .exception() + .map(|e| { + let op_state_borrow = op_state.borrow(); + let ctx: &Ctx = op_state_borrow.get_ctx(); + Box::new(crate::error::parse_js_error( + deno_core::error::JsError::from_v8_exception(scope, e), + ctx, + )) + }) + .unwrap_or_else(|| Error::internal("force failed".into())) + })?; + + let symbols = &self.symbols.local(scope); + Ok(to_value(forced, scope, symbols)) + } + fn get_symbols(scope: &ScopeRef) -> Result { let global = scope.get_current_context().global(scope); let nix_key = v8::String::new(scope, "Nix") @@ -267,6 +360,61 @@ impl Runtime { is_cycle, }) } + + fn get_cached_functions(scope: &ScopeRef) -> Result { + let global = scope.get_current_context().global(scope); + let nix_key = v8::String::new(scope, "Nix") + .ok_or_else(|| Error::internal("failed to create V8 String".into()))?; + let nix_obj = global + .get(scope, nix_key.into()) + .ok_or_else(|| Error::internal("failed to get global Nix object".into()))? + .to_object(scope) + .ok_or_else(|| { + Error::internal("failed to convert global Nix Value to object".into()) + })?; + + let get_fn = |name: &str| -> Result> { + let key = v8::String::new(scope, name) + .ok_or_else(|| Error::internal("failed to create V8 String".into()))?; + let val = nix_obj + .get(scope, key.into()) + .ok_or_else(|| Error::internal(format!("failed to get Nix.{name}")))?; + let func = val + .try_cast::() + .map_err(|err| Error::internal(format!("Nix.{name} is not a function ({err})")))?; + Ok(v8::Global::new(scope, func)) + }; + + let exec_bytecode = get_fn("execBytecode")?; + let force_fn = get_fn("force")?; + let force_shallow_fn = get_fn("forceShallow")?; + let force_deep_fn = get_fn("forceDeep")?; + + let strings_key = v8::String::new(scope, "strings") + .ok_or_else(|| Error::internal("failed to create V8 String".into()))?; + let strings_array = nix_obj + .get(scope, strings_key.into()) + .ok_or_else(|| Error::internal("failed to get Nix.strings".into()))? + .try_cast::() + .map_err(|err| Error::internal(format!("Nix.strings is not an array ({err})")))?; + + let constants_key = v8::String::new(scope, "constants") + .ok_or_else(|| Error::internal("failed to create V8 String".into()))?; + let constants_array = nix_obj + .get(scope, constants_key.into()) + .ok_or_else(|| Error::internal("failed to get Nix.constants".into()))? + .try_cast::() + .map_err(|err| Error::internal(format!("Nix.constants is not an array ({err})")))?; + + Ok(CachedFunctions { + exec_bytecode, + force_fn, + force_shallow_fn, + force_deep_fn, + strings_array: v8::Global::new(scope, strings_array), + constants_array: v8::Global::new(scope, constants_array), + }) + } } struct GlobalSymbols { @@ -297,6 +445,51 @@ struct LocalSymbols<'a> { is_cycle: v8::Local<'a, v8::Symbol>, } +struct CachedFunctions { + exec_bytecode: v8::Global, + force_fn: v8::Global, + force_shallow_fn: v8::Global, + force_deep_fn: v8::Global, + strings_array: v8::Global, + constants_array: v8::Global, +} + +pub(crate) enum ForceMode { + Force, + ForceShallow, + ForceDeep, +} + +fn sync_global_tables( + scope: &ScopeRef, + cached: &CachedFunctions, + ctx: &mut Ctx, +) { + let (new_strings, new_constants, strings_base, constants_base) = ctx.take_unsynced(); + + if !new_strings.is_empty() { + let s_array = v8::Local::new(scope, &cached.strings_array); + for (i, s) in new_strings.iter().enumerate() { + let idx = (strings_base + i) as u32; + #[allow(clippy::unwrap_used)] + let val = v8::String::new(scope, s).unwrap(); + s_array.set_index(scope, idx, val.into()); + } + } + + if !new_constants.is_empty() { + let k_array = v8::Local::new(scope, &cached.constants_array); + for (i, c) in new_constants.iter().enumerate() { + let idx = (constants_base + i) as u32; + let val: v8::Local = match c { + Constant::Int(n) => v8::BigInt::new_from_i64(scope, *n).into(), + Constant::Float(bits) => v8::Number::new(scope, f64::from_bits(*bits)).into(), + }; + k_array.set_index(scope, idx, val); + } + } +} + fn to_value<'a>( val: LocalValue<'a>, scope: &ScopeRef<'a, '_>, diff --git a/nix-js/src/runtime/ops.rs b/nix-js/src/runtime/ops.rs index aa5de89..9651251 100644 --- a/nix-js/src/runtime/ops.rs +++ b/nix-js/src/runtime/ops.rs @@ -10,6 +10,7 @@ use regex::Regex; use rust_embed::Embed; use super::{NixRuntimeError, OpStateExt, RuntimeContext}; +use crate::bytecode::{Bytecode, Constant}; use crate::error::Source; use crate::store::Store as _; @@ -79,35 +80,67 @@ fn new_simple_jserror(msg: String) -> Box { .into() } -struct Compiled(String); -impl<'a> ToV8<'a> for Compiled { +struct BytecodeRet { + bytecode: Bytecode, + new_strings: *const [String], + new_constants: *const [Constant], + strings_base: usize, + constants_base: usize, +} + +impl<'a> ToV8<'a> for BytecodeRet { type Error = Box; + #[allow(clippy::unwrap_used)] fn to_v8<'i>( self, scope: &mut v8::PinScope<'a, 'i>, ) -> std::result::Result, Self::Error> { - let Ok(script) = self.0.to_v8(scope); - let Some(source) = script.to_string(scope) else { - unsafe { std::hint::unreachable_unchecked() } - }; - let tc = std::pin::pin!(v8::TryCatch::new(scope)); - let mut scope = tc.init(); - let Some(compiled) = v8::Script::compile(&scope, source, None) else { - let msg = scope - .exception() - .map(|e| e.to_rust_string_lossy(&scope)) - .unwrap_or_else(|| "failed to compile code".into()); - return Err(new_simple_jserror(msg)); - }; - match compiled.run(&scope) { - Some(val) => Ok(val), - None => Err(scope - .exception() - .map(|e| JsError::from_v8_exception(&mut scope, e)) - .unwrap_or_else(|| { - new_simple_jserror("script execution failed unexpectedly".into()) - })), + let global = scope.get_current_context().global(scope); + let nix_key = v8::String::new(scope, "Nix") + .ok_or_else(|| new_simple_jserror("failed to create v8 string".into()))?; + let nix_obj = global + .get(scope, nix_key.into()) + .ok_or_else(|| new_simple_jserror("failed to get Nix global".into()))? + .to_object(scope) + .ok_or_else(|| new_simple_jserror("Nix is not an object".into()))?; + + let s_key = v8::String::new(scope, "strings").unwrap(); + let s_array: v8::Local = nix_obj + .get(scope, s_key.into()) + .unwrap() + .try_into() + .unwrap(); + for (i, s) in unsafe { &*self.new_strings }.iter().enumerate() { + let idx = (self.strings_base + i) as u32; + let val = v8::String::new(scope, s).unwrap(); + s_array.set_index(scope, idx, val.into()); } + + let k_key = v8::String::new(scope, "constants").unwrap(); + let k_array: v8::Local = nix_obj + .get(scope, k_key.into()) + .unwrap() + .try_into() + .unwrap(); + for (i, c) in unsafe { &*self.new_constants }.iter().enumerate() { + let idx = (self.constants_base + i) as u32; + let val: v8::Local = match c { + Constant::Int(n) => v8::BigInt::new_from_i64(scope, *n).into(), + Constant::Float(bits) => v8::Number::new(scope, f64::from_bits(*bits)).into(), + }; + k_array.set_index(scope, idx, val); + } + + let store = v8::ArrayBuffer::new_backing_store_from_boxed_slice(self.bytecode.code); + let ab = v8::ArrayBuffer::with_backing_store(scope, &store.make_shared()); + let u8a = v8::Uint8Array::new(scope, ab, 0, ab.byte_length()) + .ok_or_else(|| new_simple_jserror("failed to create Uint8Array".into()))?; + + let dir = v8::String::new(scope, &self.bytecode.current_dir) + .ok_or_else(|| new_simple_jserror("failed to create dir string".into()))?; + + let arr = v8::Array::new_with_elements(scope, &[u8a.into(), dir.into()]); + Ok(arr.into()) } } @@ -115,7 +148,7 @@ impl<'a> ToV8<'a> for Compiled { pub(super) fn op_import( state: &mut OpState, #[string] path: String, -) -> Result { +) -> Result { let _span = tracing::info_span!("op_import", path = %path).entered(); let ctx: &mut Ctx = state.get_ctx_mut(); @@ -131,8 +164,17 @@ pub(super) fn op_import( .into(), ); ctx.add_source(source.clone()); - let code = ctx.compile(source).map_err(|err| err.to_string())?; - return Ok(Compiled(code)); + let bytecode = ctx + .compile_bytecode(source) + .map_err(|err| err.to_string())?; + let (new_strings, new_constants, strings_base, constants_base) = ctx.get_unsynced(); + return Ok(BytecodeRet { + bytecode, + new_strings, + new_constants, + strings_base, + constants_base, + }); } else { return Err(format!("Corepkg not found: {}", corepkg_name).into()); } @@ -156,17 +198,25 @@ pub(super) fn op_import( tracing::debug!("Compiling file"); ctx.add_source(source.clone()); - let code = ctx.compile(source).map_err(|err| err.to_string())?; - Ok(Compiled(code)) + let bytecode = ctx + .compile_bytecode(source) + .map_err(|err| err.to_string())?; + let (new_strings, new_constants, strings_base, constants_base) = ctx.get_unsynced(); + Ok(BytecodeRet { + bytecode, + new_strings, + new_constants, + strings_base, + constants_base, + }) } -#[deno_core::op2] -#[string] +#[deno_core::op2(reentrant)] pub(super) fn op_scoped_import( state: &mut OpState, #[string] path: String, - #[scoped] scope: Vec, -) -> Result { + #[serde] scope: Vec, +) -> Result { let _span = tracing::info_span!("op_scoped_import", path = %path).entered(); let ctx: &mut Ctx = state.get_ctx_mut(); @@ -185,18 +235,26 @@ pub(super) fn op_scoped_import( tracing::debug!("Compiling file for scoped import"); ctx.add_source(source.clone()); - Ok(ctx - .compile_scoped(source, scope) - .map_err(|err| err.to_string())?) + let bytecode = ctx + .compile_bytecode_scoped(source, scope) + .map_err(|err| err.to_string())?; + let (new_strings, new_constants, strings_base, constants_base) = ctx.get_unsynced(); + Ok(BytecodeRet { + bytecode, + new_strings, + new_constants, + strings_base, + constants_base, + }) } -#[deno_core::op2] +#[deno_core::op2(reentrant)] #[string] pub(super) fn op_read_file(#[string] path: String) -> Result { Ok(std::fs::read_to_string(&path).map_err(|e| format!("Failed to read {}: {}", path, e))?) } -#[deno_core::op2(fast)] +#[deno_core::op2(fast, reentrant)] pub(super) fn op_path_exists(#[string] path: String) -> bool { let must_be_dir = path.ends_with('/') || path.ends_with("/."); let p = Path::new(&path); @@ -211,7 +269,7 @@ pub(super) fn op_path_exists(#[string] path: String) -> bool { } } -#[deno_core::op2] +#[deno_core::op2(reentrant)] #[string] pub(super) fn op_read_file_type(#[string] path: String) -> Result { let path = Path::new(&path); @@ -232,7 +290,7 @@ pub(super) fn op_read_file_type(#[string] path: String) -> Result { Ok(type_str.to_string()) } -#[deno_core::op2] +#[deno_core::op2(reentrant)] pub(super) fn op_read_dir(#[string] path: String) -> Result> { let path = Path::new(&path); @@ -273,7 +331,7 @@ pub(super) fn op_read_dir(#[string] path: String) -> Result String { use sha2::{Digest, Sha256}; @@ -341,7 +399,7 @@ impl<'a> ToV8<'a> for StringOrU32 { } } -#[deno_core::op2] +#[deno_core::op2(reentrant)] pub(super) fn op_decode_span( state: &mut OpState, #[smi] span_id: u32, @@ -390,7 +448,7 @@ mod private { } use private::*; -#[deno_core::op2] +#[deno_core::op2(reentrant)] pub(super) fn op_parse_hash( #[string] hash_str: String, #[string] algo: Option, @@ -416,7 +474,7 @@ pub(super) fn op_parse_hash( }) } -#[deno_core::op2] +#[deno_core::op2(reentrant)] #[string] pub(super) fn op_add_path( state: &mut OpState, @@ -425,11 +483,12 @@ pub(super) fn op_add_path( recursive: bool, #[string] sha256: Option, ) -> Result { - use nix_compat::nixhash::{HashAlgo, NixHash}; - use sha2::{Digest, Sha256}; use std::fs; use std::path::Path; + use nix_compat::nixhash::{HashAlgo, NixHash}; + use sha2::{Digest, Sha256}; + let path_obj = Path::new(&path); if !path_obj.exists() { @@ -495,7 +554,7 @@ pub(super) fn op_add_path( Ok(store_path) } -#[deno_core::op2] +#[deno_core::op2(reentrant)] #[string] pub(super) fn op_store_path( state: &mut OpState, @@ -516,7 +575,7 @@ pub(super) fn op_store_path( Ok(path) } -#[deno_core::op2] +#[deno_core::op2(reentrant)] #[string] pub(super) fn op_to_file( state: &mut OpState, @@ -533,7 +592,7 @@ pub(super) fn op_to_file( Ok(store_path) } -#[deno_core::op2] +#[deno_core::op2(reentrant)] #[string] pub(super) fn op_copy_path_to_store( state: &mut OpState, @@ -565,7 +624,7 @@ pub(super) fn op_copy_path_to_store( Ok(store_path) } -#[deno_core::op2] +#[deno_core::op2(reentrant)] #[string] pub(super) fn op_get_env(#[string] key: String) -> Result { match std::env::var(key) { @@ -575,7 +634,7 @@ pub(super) fn op_get_env(#[string] key: String) -> Result { } } -#[deno_core::op2] +#[deno_core::op2(reentrant)] pub(super) fn op_walk_dir(#[string] path: String) -> Result> { fn walk_recursive( base: &Path, @@ -629,7 +688,7 @@ pub(super) fn op_walk_dir(#[string] path: String) -> Result( state: &mut OpState, @@ -639,9 +698,10 @@ pub(super) fn op_add_filtered_path( #[string] sha256: Option, #[scoped] include_paths: Vec, ) -> Result { + use std::fs; + use nix_compat::nixhash::{HashAlgo, NixHash}; use sha2::{Digest, Sha256}; - use std::fs; let src = Path::new(&src_path); if !src.exists() { @@ -735,7 +795,7 @@ pub(super) fn op_add_filtered_path( Ok(store_path) } -#[deno_core::op2] +#[deno_core::op2(reentrant)] pub(super) fn op_match( state: &mut OpState, #[string] regex: String, @@ -759,7 +819,7 @@ pub(super) fn op_match( } } -#[deno_core::op2] +#[deno_core::op2(reentrant)] pub(super) fn op_split( state: &mut OpState, #[string] regex: String, @@ -925,14 +985,14 @@ fn toml_to_nix(value: toml::Value) -> Result { } } -#[deno_core::op2] +#[deno_core::op2(reentrant)] pub(super) fn op_from_json(#[string] json_str: String) -> Result { let parsed: serde_json::Value = serde_json::from_str(&json_str) .map_err(|e| NixRuntimeError::from(format!("builtins.fromJSON: {e}")))?; Ok(json_to_nix(parsed)) } -#[deno_core::op2] +#[deno_core::op2(reentrant)] pub(super) fn op_from_toml(#[string] toml_str: String) -> Result { let parsed: toml::Value = toml::from_str(&toml_str) .map_err(|e| NixRuntimeError::from(format!("while parsing TOML: {e}")))?; @@ -966,7 +1026,7 @@ fn output_path_name(drv_name: &str, output: &str) -> String { } } -#[deno_core::op2] +#[deno_core::op2(reentrant)] pub(super) fn op_finalize_derivation( state: &mut OpState, #[string] name: String, @@ -1180,7 +1240,7 @@ fn op_make_fixed_output_path_impl( } } -#[deno_core::op2] +#[deno_core::op2(reentrant)] #[string] pub(super) fn op_hash_string(#[string] algo: String, #[string] data: String) -> Result { use sha2::{Digest, Sha256, Sha512}; @@ -1217,7 +1277,7 @@ pub(super) fn op_hash_string(#[string] algo: String, #[string] data: String) -> Ok(hex::encode(hash_bytes)) } -#[deno_core::op2] +#[deno_core::op2(reentrant)] #[string] pub(super) fn op_hash_file(#[string] algo: String, #[string] path: String) -> Result { let data = std::fs::read(&path) @@ -1257,7 +1317,7 @@ pub(super) fn op_hash_file(#[string] algo: String, #[string] path: String) -> Re Ok(hex::encode(hash_bytes)) } -#[deno_core::op2] +#[deno_core::op2(reentrant)] #[string] pub(super) fn op_convert_hash( #[string] hash: &str, @@ -1830,7 +1890,7 @@ impl<'a> FromV8<'a> for ToXmlResult { } } -#[deno_core::op2] +#[deno_core::op2(reentrant)] pub(super) fn op_to_xml(#[scoped] value: ToXmlResult) -> (String, Vec) { (value.xml, value.context) }